Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Maas instance plugin #411

Merged
merged 6 commits into from
Feb 28, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ endif
$(call build_binary,infrakit-instance-file,github.com/docker/infrakit/examples/instance/file)
$(call build_binary,infrakit-instance-terraform,github.com/docker/infrakit/examples/instance/terraform)
$(call build_binary,infrakit-instance-vagrant,github.com/docker/infrakit/examples/instance/vagrant)
$(call build_binary,infrakit-instance-maas,github.com/docker/infrakit/examples/instance/maas)

install:
@echo "+ $@"
Expand Down
68 changes: 68 additions & 0 deletions examples/instance/maas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Infrakit MaaS Instance Plugin
An example of [Infrakit](https://github.com/docker/infrakit) Instance Pluging for [Canonical MaaS](https://maas.io/) node.
Purpose of this plugin is to support Bare metal deploy with Infrakit.
## Go get
`$go get github.com/YujiOshima/infrakit-maas-plugin`

## Get start
Requires:
* Needs virtualbox % sudo apt-get install virtualbox
* Needs vagrant 1.6.x
* Needs ansible installed on host machine
* MaaS Server
* API Key ( See https://maas.ubuntu.com/docs/maascli.html )
* MaaS Nodes (You need all node be `Ready` state. Register and commision manually.)
* Spec file ( example: maas-vanilla.yml )

As this is only demo, we use virtual box. we should be use BareMetal Server Cluster with MaaS

### Set up MaaS Cluster

```
$ git clone https://github.com/YujiOshima/vagrant-maas-in-a-box
$ cd vagrant-maas-in-a-box
$ vagrant up maas
```

In your browser visit http://localhost:8080/MAAS

Default login and password: `admin` `pass`

Import boot images in `Images` tab.

! Wait for import images. It takes some time. !

Set up nodes.

```
./setup-nodes.sh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file included in this PR? I don't see it. Would you mind including it?

Copy link
Contributor Author

@YujiOshima YujiOshima Feb 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I update the Readme. Now I have only a simple document, but I will enhance it with the addition of functions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I was confused. This is a file from your repo... I see now, so there's no need to include this.

```

3 nodes are enlisted in MaaS.

check `Nodes` tab.

Get MaaS API KEY. with web browser, or run `APIKEY=vagrant ssh maas -c "sudo maas-region-admin apikey --username admin"`

### Run Infrakit group, vanilla flavor plugin.

```
$ build/infrakit-group-default
```

```
$ build/infrakit-flavor-vanilla
```

Run MaaS instance plugin.

```
$ $GOPATH/bin/infrakit-maas-plugin --apikey $APIKEY --url http://localhost:8080/MAAS
INFO[0000] Listening at: /home/ubuntu/.infrakit/plugins/instance-maas
```
Group commit!

```
$ build/infrakit group commit $GOPATH/src/github.com/YujiOshima/infrakit-maas-plugin/maas-vanilla.yml
```

282 changes: 282 additions & 0 deletions examples/instance/maas/instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package main

import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"time"

"github.com/docker/infrakit/pkg/spi/instance"
"github.com/docker/infrakit/pkg/types"
maas "github.com/juju/gomaasapi"
"net/url"
)

// NewMaasPlugin creates an instance plugin for MaaS.
func NewMaasPlugin(dir string, key string, url string, version string) instance.Plugin {
authClient, err := maas.NewAuthenticatedClient(url, key, version)
if err != nil {
return nil
}
maasobj := maas.NewMAAS(*authClient)
return &maasPlugin{MaasfilesDir: dir, MaasObj: maasobj}
}

type maasPlugin struct {
MaasfilesDir string
MaasObj *maas.MAASObject
}

// Validate performs local validation on a provision request.
func (m maasPlugin) Validate(req *types.Any) error {
return nil
}

func (m maasPlugin) convertSpecToMaasParam(spec map[string]interface{}) url.Values {
param := url.Values{}
return param
}

func (m maasPlugin) checkDuplicate(hostname string) (bool, error) {
files, err := ioutil.ReadDir(m.MaasfilesDir)
if err != nil {
return false, err
}

for _, file := range files {
if !file.IsDir() {
continue
}
machineDir := path.Join(m.MaasfilesDir, file.Name())
hnData, err := ioutil.ReadFile(path.Join(machineDir, "MachineName"))
if err != nil {
if os.IsNotExist(err) {
continue
}
return false, err
}
if hostname == string(hnData) {
return true, nil
}
}
return false, nil
}

// Provision creates a new instance.
func (m maasPlugin) Provision(spec instance.Spec) (*instance.ID, error) {

var properties map[string]interface{}

if spec.Properties != nil {
if err := spec.Properties.Decode(&properties); err != nil {
return nil, fmt.Errorf("Invalid instance properties: %s", err)
}
}
nodeListing := m.MaasObj.GetSubObject("nodes")
jsonResponse, err := nodeListing.CallPost("acquire", m.convertSpecToMaasParam(properties))
if err != nil {
return nil, err
}
acquiredNode, err := jsonResponse.GetMAASObject()
if err != nil {
return nil, err
}
listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
if err != nil {
return nil, err
}
listNodes, err := listNodeObjects.GetArray()
if err != nil {
return nil, err
}
notDuplicate := false
for range listNodes {
hostname, err := acquiredNode.GetField("hostname")
if err != nil {
return nil, err
}
isdup, err := m.checkDuplicate(hostname)
if err != nil {
return nil, err
}
if isdup {
jsonResponse, err = nodeListing.CallPost("acquire", m.convertSpecToMaasParam(properties))
if err != nil {
return nil, err
}
acquiredNode, err = jsonResponse.GetMAASObject()
if err != nil {
return nil, err
}
} else {
notDuplicate = true
break
}
}
if !notDuplicate {
return nil, errors.New("Failed to aquire node")
}
isAcquired := make(chan bool)
go func() {
for state, err := acquiredNode.GetField("substatus_name"); state != "Allocated" && err == nil; state, err = acquiredNode.GetField("substatus_name") {
time.Sleep(100)
}
isAcquired <- true
return
}()
<-isAcquired
params := url.Values{}
if _, err = acquiredNode.CallPost("start", params); err != nil {
return nil, err
}
hostname, err := acquiredNode.GetField("hostname")
if err != nil {
return nil, err
}
id := instance.ID(hostname)
machineDir, err := ioutil.TempDir(m.MaasfilesDir, "infrakit-")
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(path.Join(machineDir, "MachineName"), []byte(hostname), 0755); err != nil {
return nil, err
}
tagData, err := types.AnyValue(spec.Tags)
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(path.Join(machineDir, "tags"), tagData.Bytes(), 0666); err != nil {
return nil, err
}
if spec.LogicalID != nil {
if err := ioutil.WriteFile(path.Join(machineDir, "ip"), []byte(*spec.LogicalID), 0666); err != nil {
return nil, err
}
}
return &id, nil
}

// Label labels the instance
func (m maasPlugin) Label(instance instance.ID, labels map[string]string) error {
machineDir := path.Join(m.MaasfilesDir, string(instance))
tagFile := path.Join(machineDir, "tags")
buff, err := ioutil.ReadFile(tagFile)
if err != nil {
return err
}

tags := map[string]string{}
err = types.AnyBytes(buff).Decode(&tags)
if err != nil {
return err
}

for k, v := range labels {
tags[k] = v
}

encoded, err := types.AnyValue(tags)
if err != nil {
return err
}
return ioutil.WriteFile(tagFile, encoded.Bytes(), 0666)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files are written locally to store tags... is there a way in MAAS to tag resources and query by tags? https://docs.ubuntu.com/maas/2.1/en/intro-concepts#tags

If we can reduce amount of state we need store locally, the better. If there are ways to do tagging via the MAAS api, then we should consider making that change in a future PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is a better implementation. This time we implemented it in a simple way that does not depend on the api version, but we will change the information to keep it in a robust way without duplication.

}

// Destroy terminates an existing instance.
func (m maasPlugin) Destroy(id instance.ID) error {
fmt.Println("Destroying ", id)
nodeListing := m.MaasObj.GetSubObject("nodes")
listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
if err != nil {
return err
}
listNodes, err := listNodeObjects.GetArray()
for _, nodeObj := range listNodes {
node, err := nodeObj.GetMAASObject()
hostname, err := node.GetField("hostname")
if hostname == string(id) {
if state, _ := node.GetField("substatus_name"); state == "Deploying" {
params := url.Values{}
if _, err = node.CallPost("abort_operation", params); err != nil {
return err
}
}
params := url.Values{}
if _, err = node.CallPost("release", params); err != nil {
return err
}
}
}
files, err := ioutil.ReadDir(m.MaasfilesDir)
if err != nil {
return err
}
for _, file := range files {
if !file.IsDir() {
continue
}
machineDir := path.Join(m.MaasfilesDir, file.Name())
hostname, err := ioutil.ReadFile(path.Join(machineDir, "MachineName"))
if err != nil {
if os.IsNotExist(err) {
continue
}
return err
} else if id == instance.ID(hostname) {
if err := os.RemoveAll(machineDir); err != nil {
return err
}
}
}
return nil
}

// DescribeInstances returns descriptions of all instances matching all of the provided tags.
func (m maasPlugin) DescribeInstances(tags map[string]string) ([]instance.Description, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above -- is there a way in MAAS to query by tags of the nodes? Using files here is ok but we'd have to make sure these files are highly available.

files, err := ioutil.ReadDir(m.MaasfilesDir)
if err != nil {
return nil, err
}
descriptions := []instance.Description{}
for _, file := range files {
if !file.IsDir() {
continue
}
machineDir := path.Join(m.MaasfilesDir, file.Name())
tagData, err := ioutil.ReadFile(path.Join(machineDir, "tags"))
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
machineTags := map[string]string{}
if err := types.AnyBytes(tagData).Decode(&machineTags); err != nil {
return nil, err
}
allMatched := true
for k, v := range tags {
value, exists := machineTags[k]
if !exists || v != value {
allMatched = false
break
}
}
if allMatched {
hostname, err := ioutil.ReadFile(path.Join(machineDir, "MachineName"))
if err == nil {
} else {
if !os.IsNotExist(err) {
return nil, err
}
}
descriptions = append(descriptions, instance.Description{
ID: instance.ID(hostname),
Tags: machineTags,
})
}
}
return descriptions, nil
}
24 changes: 24 additions & 0 deletions examples/instance/maas/maas-vanilla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is named .yml but the content is a JSON...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh..This is a mistake. Fixed.

"ID": "cattle",
"Properties": {
"Allocation": {
"Size": 2
},
"Instance": {
"Plugin": "instance-maas",
"Properties": {
}
},
"Flavor": {
"Plugin": "flavor-vanilla",
"Properties": {
"Init": [
],
"Tags": {
"tier": "web",
"project": "infrakit"
}
}
}
}
}
Loading