├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── bin └── main.go ├── circle.yml ├── cloudstack.go ├── cloudstack_test.go ├── glide.lock ├── glide.yaml └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | dist/ 3 | .envrc 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.8.3 2 | 3 | RUN go get github.com/mitchellh/gox \ 4 | github.com/Masterminds/glide \ 5 | github.com/tcnksm/ghr 6 | 7 | # Trust everyone to allow glide to execute `hg paths` on mounted volumes 8 | # https://www.mercurial-scm.org/wiki/Trust 9 | RUN echo "[trusted]\nusers=*" > /etc/mercurial/hgrc 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test release clean 2 | 3 | GITHUB_USER := atsaki 4 | VERSION := $(shell grep -w Version version.go | awk '{print $$5}' | sed 's/"//g') 5 | 6 | TARGET_OS ?= darwin linux windows 7 | TARGET_ARCH ?= amd64 8 | 9 | MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 10 | CURRENT_DIR := $(notdir $(patsubst %/,%,$(dir $(MAKEFILE_PATH)))) 11 | PROJECT := github.com/$(GITHUB_USER)/$(CURRENT_DIR) 12 | 13 | EXECUTABLE_NAME := $(CURRENT_DIR) 14 | DOCKER_IMAGE_NAME := $(CURRENT_DIR)-build 15 | DOCKER_CONTAINER_NAME := $(DOCKER_IMAGE_NAME)-build-container 16 | 17 | default: build 18 | 19 | clean: 20 | rm -rf dist 21 | 22 | ifeq ($(USE_CONTAINER), true) 23 | build test release: 24 | docker build -t $(DOCKER_IMAGE_NAME) . 25 | 26 | docker run \ 27 | --rm \ 28 | --name $(DOCKER_CONTAINER_NAME) \ 29 | -v $(shell pwd):/go/src/$(PROJECT) \ 30 | -w /go/src/$(PROJECT) \ 31 | -e TARGET_OS \ 32 | -e TARGET_ARCH \ 33 | -e GITHUB_TOKEN \ 34 | $(DOCKER_IMAGE_NAME) \ 35 | make $@ 36 | else 37 | build: 38 | glide up 39 | GOGC=off gox -os "$(TARGET_OS)" -arch "$(TARGET_ARCH)" \ 40 | -output "dist/$(EXECUTABLE_NAME)_{{.OS}}_{{.Arch}}" ./bin 41 | 42 | test: 43 | exit 0 44 | 45 | release: clean build 46 | git tag | grep -q -w $(VERSION) || git tag $(VERSION) 47 | 48 | ghr --repository $(CURRENT_DIR) \ 49 | --username $(GITHUB_USER) \ 50 | --prerelease \ 51 | --replace \ 52 | $(VERSION) dist/ 53 | endif 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Machine CloudStack Driver 2 | 3 | [![Circle CI](https://circleci.com/gh/atsaki/docker-machine-driver-cloudstack.svg?style=svg)](https://circleci.com/gh/atsaki/docker-machine-driver-cloudstack) 4 | 5 | Docker Machine CloudStack Driver is a driver for [Docker Machine](https://docs.docker.com/machine/). 6 | It allows to create Docker hosts on [Apache CloudStack](https://cloudstack.apache.org/) and 7 | [Accelerite CloudPlatform](http://cloudplatform.accelerite.com/). 8 | 9 | ## Requirements 10 | 11 | * [Docker Machine](https://docs.docker.com/machine/) 0.5.1 or later 12 | 13 | ## Installation 14 | 15 | Download the binary from follwing link and put it within your PATH (ex. `/usr/local/bin`) 16 | 17 | https://github.com/atsaki/docker-machine-driver-cloudstack/releases/latest 18 | 19 | ### Homebrew 20 | 21 | OSX User can use [Homebrew](http://brew.sh/). 22 | 23 | ```bash 24 | brew tap atsaki/docker-machine-driver-cloudstack 25 | brew install docker-machine-driver-cloudstack 26 | ``` 27 | ## Usage 28 | 29 | ```bash 30 | docker-machine create -d cloudstack \ 31 | --cloudstack-api-url CLOUDSTACK_API_URL \ 32 | --cloudstack-api-key CLOUDSTACK_API_KEY \ 33 | --cloudstack-secret-key CLOUDSTACK_SECRET_KEY \ 34 | --cloudstack-template "Ubuntu Server 14.04" \ 35 | --cloudstack-zone "zone01" \ 36 | --cloudstack-service-offering "Small" \ 37 | --cloudstack-expunge \ 38 | docker-machine 39 | ``` 40 | 41 | ## Acknowledgement 42 | 43 | The driver is originally written by [@svanharmelen](https://github.com/svanharmelen). 44 | 45 | ## Author 46 | 47 | Atushi Sasaki([@atsaki](https://github.com/atsaki)) 48 | -------------------------------------------------------------------------------- /bin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/atsaki/docker-machine-driver-cloudstack" 5 | "github.com/docker/machine/libmachine/drivers/plugin" 6 | ) 7 | 8 | func main() { 9 | plugin.RegisterDriver(cloudstack.NewDriver("", "")) 10 | } 11 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | timezone: 3 | Asia/Tokyo 4 | services: 5 | - docker 6 | 7 | dependencies: 8 | override: 9 | - USE_CONTAINER=true make build 10 | cache_directories: 11 | - vendor 12 | 13 | test: 14 | override: 15 | - USE_CONTAINER=true make test 16 | 17 | deployment: 18 | pre_release: 19 | branch: master 20 | commands: 21 | - USE_CONTAINER=true make release 22 | -------------------------------------------------------------------------------- /cloudstack.go: -------------------------------------------------------------------------------- 1 | package cloudstack 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | 9 | "github.com/docker/machine/libmachine/drivers" 10 | "github.com/docker/machine/libmachine/log" 11 | "github.com/docker/machine/libmachine/mcnflag" 12 | "github.com/docker/machine/libmachine/ssh" 13 | "github.com/docker/machine/libmachine/state" 14 | "github.com/xanzy/go-cloudstack/cloudstack" 15 | ) 16 | 17 | const ( 18 | driverName = "cloudstack" 19 | dockerPort = 2376 20 | swarmPort = 3376 21 | ) 22 | 23 | type configError struct { 24 | option string 25 | } 26 | 27 | func (e *configError) Error() string { 28 | return fmt.Sprintf("cloudstack driver requires the --cloudstack-%s option", e.option) 29 | } 30 | 31 | type Driver struct { 32 | *drivers.BaseDriver 33 | Id string 34 | ApiURL string 35 | ApiKey string 36 | SecretKey string 37 | HTTPGETOnly bool 38 | JobTimeOut int64 39 | UsePrivateIP bool 40 | UsePortForward bool 41 | PublicIP string 42 | PublicIPID string 43 | DisassociatePublicIP bool 44 | SSHKeyPair string 45 | PrivateIP string 46 | CIDRList []string 47 | FirewallRuleIds []string 48 | Expunge bool 49 | Template string 50 | TemplateID string 51 | ServiceOffering string 52 | ServiceOfferingID string 53 | Network string 54 | NetworkID string 55 | Zone string 56 | ZoneID string 57 | NetworkType string 58 | UserDataFile string 59 | UserData string 60 | } 61 | 62 | // GetCreateFlags registers the flags this driver adds to 63 | // "docker hosts create" 64 | func (d *Driver) GetCreateFlags() []mcnflag.Flag { 65 | return []mcnflag.Flag{ 66 | mcnflag.StringFlag{ 67 | Name: "cloudstack-api-url", 68 | Usage: "CloudStack API URL", 69 | EnvVar: "CLOUDSTACK_API_URL", 70 | }, 71 | mcnflag.StringFlag{ 72 | Name: "cloudstack-api-key", 73 | Usage: "CloudStack API key", 74 | EnvVar: "CLOUDSTACK_API_KEY", 75 | }, 76 | mcnflag.StringFlag{ 77 | Name: "cloudstack-secret-key", 78 | Usage: "CloudStack API secret key", 79 | EnvVar: "CLOUDSTACK_SECRET_KEY", 80 | }, 81 | mcnflag.BoolFlag{ 82 | Name: "cloudstack-http-get-only", 83 | Usage: "Only use HTTP GET to execute CloudStack API", 84 | EnvVar: "CLOUDSTACK_HTTP_GET_ONLY", 85 | }, 86 | mcnflag.IntFlag{ 87 | Name: "cloudstack-timeout", 88 | Usage: "time(seconds) allowed to complete async job", 89 | EnvVar: "CLOUDSTACK_TIMEOUT", 90 | Value: 300, 91 | }, 92 | mcnflag.BoolFlag{ 93 | Name: "cloudstack-use-private-address", 94 | Usage: "Use a private IP to access the machine", 95 | }, 96 | mcnflag.BoolFlag{ 97 | Name: "cloudstack-use-port-forward", 98 | Usage: "Use port forwarding rule to access the machine", 99 | }, 100 | mcnflag.StringFlag{ 101 | Name: "cloudstack-public-ip", 102 | Usage: "CloudStack Public IP", 103 | }, 104 | mcnflag.StringFlag{ 105 | Name: "cloudstack-ssh-user", 106 | Usage: "CloudStack SSH user", 107 | Value: "root", 108 | }, 109 | mcnflag.StringSliceFlag{ 110 | Name: "cloudstack-cidr", 111 | Usage: "Source CIDR to give access to the machine. default 0.0.0.0/0", 112 | }, 113 | mcnflag.BoolFlag{ 114 | Name: "cloudstack-expunge", 115 | Usage: "Whether or not to expunge the machine upon removal", 116 | }, 117 | mcnflag.StringFlag{ 118 | Name: "cloudstack-template", 119 | Usage: "CloudStack template", 120 | }, 121 | mcnflag.StringFlag{ 122 | Name: "cloudstack-service-offering", 123 | Usage: "CloudStack service offering", 124 | }, 125 | mcnflag.StringFlag{ 126 | Name: "cloudstack-network", 127 | Usage: "CloudStack network", 128 | }, 129 | mcnflag.StringFlag{ 130 | Name: "cloudstack-zone", 131 | Usage: "CloudStack zone", 132 | }, 133 | mcnflag.StringFlag{ 134 | Name: "cloudstack-userdata-file", 135 | Usage: "CloudStack Userdata file", 136 | }, 137 | } 138 | } 139 | 140 | func NewDriver(hostName, storePath string) drivers.Driver { 141 | 142 | driver := &Driver{ 143 | BaseDriver: &drivers.BaseDriver{ 144 | MachineName: hostName, 145 | StorePath: storePath, 146 | }, 147 | FirewallRuleIds: []string{}, 148 | } 149 | return driver 150 | } 151 | 152 | // DriverName returns the name of the driver as it is registered 153 | func (d *Driver) DriverName() string { 154 | return driverName 155 | } 156 | 157 | func (d *Driver) GetSSHHostname() (string, error) { 158 | return d.GetIP() 159 | } 160 | 161 | func (d *Driver) GetSSHUsername() string { 162 | if d.SSHUser == "" { 163 | d.SSHUser = "root" 164 | } 165 | return d.SSHUser 166 | } 167 | 168 | // SetConfigFromFlags configures the driver with the object that was returned 169 | // by RegisterCreateFlags 170 | func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { 171 | d.ApiURL = flags.String("cloudstack-api-url") 172 | d.ApiKey = flags.String("cloudstack-api-key") 173 | d.SecretKey = flags.String("cloudstack-secret-key") 174 | d.UsePrivateIP = flags.Bool("cloudstack-use-private-address") 175 | d.UsePortForward = flags.Bool("cloudstack-use-port-forward") 176 | d.HTTPGETOnly = flags.Bool("cloudstack-http-get-only") 177 | d.JobTimeOut = int64(flags.Int("cloudstack-timeout")) 178 | d.SSHUser = flags.String("cloudstack-ssh-user") 179 | d.CIDRList = flags.StringSlice("cloudstack-cidr") 180 | d.Expunge = flags.Bool("cloudstack-expunge") 181 | 182 | if err := d.setZone(flags.String("cloudstack-zone")); err != nil { 183 | return err 184 | } 185 | if err := d.setTemplate(flags.String("cloudstack-template")); err != nil { 186 | return err 187 | } 188 | if err := d.setServiceOffering(flags.String("cloudstack-service-offering")); err != nil { 189 | return err 190 | } 191 | if err := d.setNetwork(flags.String("cloudstack-network")); err != nil { 192 | return err 193 | } 194 | if err := d.setPublicIP(flags.String("cloudstack-public-ip")); err != nil { 195 | return err 196 | } 197 | if err := d.setUserData(flags.String("cloudstack-userdata-file")); err != nil { 198 | return err 199 | } 200 | 201 | d.SwarmMaster = flags.Bool("swarm-master") 202 | d.SwarmDiscovery = flags.String("swarm-discovery") 203 | 204 | d.SSHKeyPair = d.MachineName 205 | 206 | if d.ApiURL == "" { 207 | return &configError{option: "api-url"} 208 | } 209 | 210 | if d.ApiKey == "" { 211 | return &configError{option: "api-key"} 212 | } 213 | 214 | if d.SecretKey == "" { 215 | return &configError{option: "secret-key"} 216 | } 217 | 218 | if d.Template == "" { 219 | return &configError{option: "template"} 220 | } 221 | 222 | if d.ServiceOffering == "" { 223 | return &configError{option: "service-offering"} 224 | } 225 | 226 | if d.Zone == "" { 227 | return &configError{option: "zone"} 228 | } 229 | 230 | if len(d.CIDRList) == 0 { 231 | d.CIDRList = []string{"0.0.0.0/0"} 232 | } 233 | 234 | d.DisassociatePublicIP = false 235 | 236 | return nil 237 | } 238 | 239 | // GetURL returns a Docker compatible host URL for connecting to this host 240 | // e.g. tcp://1.2.3.4:2376 241 | func (d *Driver) GetURL() (string, error) { 242 | ip, err := d.GetIP() 243 | if err != nil { 244 | return "", err 245 | } 246 | return fmt.Sprintf("tcp://%s:%d", ip, dockerPort), nil 247 | } 248 | 249 | // GetIP returns the IP that this host is available at 250 | func (d *Driver) GetIP() (string, error) { 251 | if d.UsePrivateIP { 252 | return d.PrivateIP, nil 253 | } 254 | return d.PublicIP, nil 255 | } 256 | 257 | // GetState returns the state that the host is in (running, stopped, etc) 258 | func (d *Driver) GetState() (state.State, error) { 259 | cs := d.getClient() 260 | vm, count, err := cs.VirtualMachine.GetVirtualMachineByID(d.Id) 261 | if err != nil { 262 | return state.Error, err 263 | } 264 | 265 | if count == 0 { 266 | return state.None, fmt.Errorf("Machine does not exist, use create command to create it") 267 | } 268 | 269 | switch vm.State { 270 | case "Starting": 271 | return state.Starting, nil 272 | case "Running": 273 | return state.Running, nil 274 | case "Stopping": 275 | return state.Running, nil 276 | case "Stopped": 277 | return state.Stopped, nil 278 | case "Destroyed": 279 | return state.Stopped, nil 280 | case "Expunging": 281 | return state.Stopped, nil 282 | case "Migrating": 283 | return state.Paused, nil 284 | case "Error": 285 | return state.Error, nil 286 | case "Unknown": 287 | return state.Error, nil 288 | case "Shutdowned": 289 | return state.Stopped, nil 290 | } 291 | 292 | return state.None, nil 293 | } 294 | 295 | // PreCreate allows for pre-create operations to make sure a driver is ready for creation 296 | func (d *Driver) PreCreateCheck() error { 297 | 298 | if err := d.checkKeyPair(); err != nil { 299 | return err 300 | } 301 | 302 | if err := d.checkInstance(); err != nil { 303 | return err 304 | } 305 | 306 | return nil 307 | } 308 | 309 | // Create a host using the driver's config 310 | func (d *Driver) Create() error { 311 | cs := d.getClient() 312 | 313 | if err := d.createKeyPair(); err != nil { 314 | return err 315 | } 316 | 317 | p := cs.VirtualMachine.NewDeployVirtualMachineParams( 318 | d.ServiceOfferingID, d.TemplateID, d.ZoneID) 319 | p.SetName(d.MachineName) 320 | p.SetDisplayname(d.MachineName) 321 | p.SetKeypair(d.SSHKeyPair) 322 | 323 | if d.UserData != "" { 324 | p.SetUserdata(d.UserData) 325 | } 326 | 327 | if d.NetworkID != "" { 328 | p.SetNetworkids([]string{d.NetworkID}) 329 | } 330 | 331 | if d.NetworkType == "Basic" { 332 | if err := d.createSecurityGroup(); err != nil { 333 | return err 334 | } 335 | p.SetSecuritygroupnames([]string{d.MachineName}) 336 | } 337 | 338 | // Create the machine 339 | log.Info("Creating CloudStack instance...") 340 | vm, err := cs.VirtualMachine.DeployVirtualMachine(p) 341 | if err != nil { 342 | return err 343 | } 344 | 345 | d.Id = vm.Id 346 | 347 | d.PrivateIP = vm.Nic[0].Ipaddress 348 | if d.NetworkType == "Basic" { 349 | d.PublicIP = d.PrivateIP 350 | } 351 | 352 | if d.NetworkType == "Advanced" && !d.UsePrivateIP { 353 | if d.PublicIPID == "" { 354 | if err := d.associatePublicIP(); err != nil { 355 | return err 356 | } 357 | } 358 | 359 | if err := d.configureFirewallRules(); err != nil { 360 | return err 361 | } 362 | 363 | if d.UsePortForward { 364 | if err := d.configurePortForwardingRules(); err != nil { 365 | return err 366 | } 367 | } else { 368 | if err := d.enableStaticNat(); err != nil { 369 | return err 370 | } 371 | } 372 | } 373 | 374 | return nil 375 | } 376 | 377 | // Remove a host 378 | func (d *Driver) Remove() error { 379 | cs := d.getClient() 380 | p := cs.VirtualMachine.NewDestroyVirtualMachineParams(d.Id) 381 | p.SetExpunge(d.Expunge) 382 | 383 | if err := d.deleteFirewallRules(); err != nil { 384 | return err 385 | } 386 | 387 | if err := d.disassociatePublicIP(); err != nil { 388 | return err 389 | } 390 | 391 | log.Info("Removing CloudStack instance...") 392 | if _, err := cs.VirtualMachine.DestroyVirtualMachine(p); err != nil { 393 | return err 394 | } 395 | if d.NetworkType == "Basic" { 396 | if err := d.deleteSecurityGroup(); err != nil { 397 | return err 398 | } 399 | } 400 | 401 | if err := d.deleteKeyPair(); err != nil { 402 | return err 403 | } 404 | 405 | return nil 406 | } 407 | 408 | // Start a host 409 | func (d *Driver) Start() error { 410 | vmstate, err := d.GetState() 411 | if err != nil { 412 | return err 413 | } 414 | 415 | if vmstate == state.Running { 416 | log.Info("Machine is already running") 417 | return nil 418 | } 419 | 420 | if vmstate == state.Starting { 421 | log.Info("Machine is already starting") 422 | return nil 423 | } 424 | 425 | cs := d.getClient() 426 | p := cs.VirtualMachine.NewStartVirtualMachineParams(d.Id) 427 | 428 | if _, err = cs.VirtualMachine.StartVirtualMachine(p); err != nil { 429 | return err 430 | } 431 | 432 | return nil 433 | } 434 | 435 | // Stop a host gracefully 436 | func (d *Driver) Stop() error { 437 | vmstate, err := d.GetState() 438 | if err != nil { 439 | return err 440 | } 441 | 442 | if vmstate == state.Stopped { 443 | log.Info("Machine is already stopped") 444 | return nil 445 | } 446 | 447 | cs := d.getClient() 448 | p := cs.VirtualMachine.NewStopVirtualMachineParams(d.Id) 449 | 450 | if _, err = cs.VirtualMachine.StopVirtualMachine(p); err != nil { 451 | return err 452 | } 453 | 454 | return nil 455 | } 456 | 457 | // Restart a host. 458 | func (d *Driver) Restart() error { 459 | vmstate, err := d.GetState() 460 | if err != nil { 461 | return err 462 | } 463 | 464 | if vmstate == state.Stopped { 465 | return fmt.Errorf("Machine is stopped, use start command to start it") 466 | } 467 | 468 | cs := d.getClient() 469 | p := cs.VirtualMachine.NewRebootVirtualMachineParams(d.Id) 470 | 471 | if _, err = cs.VirtualMachine.RebootVirtualMachine(p); err != nil { 472 | return err 473 | } 474 | 475 | return nil 476 | } 477 | 478 | // Kill stops a host forcefully 479 | func (d *Driver) Kill() error { 480 | return d.Stop() 481 | } 482 | 483 | func (d *Driver) getClient() *cloudstack.CloudStackClient { 484 | cs := cloudstack.NewAsyncClient(d.ApiURL, d.ApiKey, d.SecretKey, false) 485 | cs.HTTPGETOnly = d.HTTPGETOnly 486 | cs.AsyncTimeout(d.JobTimeOut) 487 | return cs 488 | } 489 | 490 | func (d *Driver) setZone(zone string) error { 491 | d.Zone = zone 492 | d.ZoneID = "" 493 | d.NetworkType = "" 494 | 495 | if d.Zone == "" { 496 | return nil 497 | } 498 | 499 | cs := d.getClient() 500 | z, _, err := cs.Zone.GetZoneByName(d.Zone) 501 | if err != nil { 502 | return fmt.Errorf("Unable to get zone: %v", err) 503 | } 504 | 505 | d.ZoneID = z.Id 506 | d.NetworkType = z.Networktype 507 | 508 | log.Debugf("zone: %q", d.Zone) 509 | log.Debugf("zone id: %q", d.ZoneID) 510 | log.Debugf("network type: %q", d.NetworkType) 511 | 512 | return nil 513 | } 514 | 515 | func (d *Driver) setTemplate(template string) error { 516 | d.Template = template 517 | d.TemplateID = "" 518 | 519 | if d.Template == "" { 520 | return nil 521 | } 522 | 523 | if d.ZoneID == "" { 524 | return fmt.Errorf("Unable to get template id: zone is not set") 525 | } 526 | 527 | cs := d.getClient() 528 | templateid, counts, err := cs.Template.GetTemplateID(d.Template, "executable", d.ZoneID) 529 | if err != nil { 530 | return fmt.Errorf("Unable to get template id: %v", err) 531 | } 532 | 533 | if counts > 1 { 534 | return fmt.Errorf("Unable to specify template id") 535 | } 536 | 537 | d.TemplateID = templateid 538 | log.Debugf("template id: %q", d.TemplateID) 539 | 540 | return nil 541 | } 542 | 543 | func (d *Driver) setServiceOffering(serviceoffering string) error { 544 | d.ServiceOffering = serviceoffering 545 | d.ServiceOfferingID = "" 546 | 547 | if d.ServiceOffering == "" { 548 | return nil 549 | } 550 | 551 | cs := d.getClient() 552 | serviceofferingid, counts, err := cs.ServiceOffering.GetServiceOfferingID(d.ServiceOffering) 553 | if err != nil { 554 | return fmt.Errorf("Unable to get service offering id: %v", err) 555 | } 556 | 557 | if counts > 1 { 558 | return fmt.Errorf("Unable to specify service offering id") 559 | } 560 | 561 | d.ServiceOfferingID = serviceofferingid 562 | 563 | log.Debugf("service offering id: %q", d.ServiceOfferingID) 564 | 565 | return nil 566 | } 567 | 568 | func (d *Driver) setNetwork(network string) error { 569 | d.Network = network 570 | d.NetworkID = "" 571 | 572 | if d.Network == "" { 573 | return nil 574 | } 575 | 576 | cs := d.getClient() 577 | networkid, counts, err := cs.Network.GetNetworkID(d.Network) 578 | if err != nil { 579 | return fmt.Errorf("Unable to get network id: %v", err) 580 | } 581 | 582 | if counts > 1 { 583 | return fmt.Errorf("Unable to specify network id") 584 | } 585 | 586 | d.NetworkID = networkid 587 | 588 | log.Debugf("network id: %q", d.NetworkID) 589 | 590 | return nil 591 | } 592 | 593 | func (d *Driver) setPublicIP(publicip string) error { 594 | d.PublicIP = publicip 595 | d.PublicIPID = "" 596 | 597 | if d.PublicIP == "" { 598 | return nil 599 | } 600 | 601 | cs := d.getClient() 602 | p := cs.Address.NewListPublicIpAddressesParams() 603 | p.SetIpaddress(d.PublicIP) 604 | ips, err := cs.Address.ListPublicIpAddresses(p) 605 | if err != nil { 606 | return fmt.Errorf("Unable to get public ip id: %s", err) 607 | } 608 | if ips.Count < 1 { 609 | return fmt.Errorf("Unable to get public ip id: Not Found %s", d.PublicIP) 610 | } 611 | 612 | d.PublicIPID = ips.PublicIpAddresses[0].Id 613 | 614 | log.Debugf("public ip id: %q", d.PublicIPID) 615 | 616 | return nil 617 | } 618 | 619 | func (d *Driver) setUserData(userDataFile string) error { 620 | d.UserDataFile = userDataFile 621 | 622 | if d.UserDataFile == "" { 623 | return nil 624 | } 625 | 626 | data, err := ioutil.ReadFile(d.UserDataFile) 627 | if err != nil { 628 | return fmt.Errorf("Failed to read user data file: %s", err) 629 | } 630 | 631 | d.UserData = base64.StdEncoding.EncodeToString(data) 632 | 633 | return nil 634 | } 635 | 636 | func (d *Driver) checkKeyPair() error { 637 | cs := d.getClient() 638 | 639 | log.Infof("Checking if SSH key pair (%v) already exists...", d.SSHKeyPair) 640 | 641 | p := cs.SSH.NewListSSHKeyPairsParams() 642 | p.SetName(d.SSHKeyPair) 643 | res, err := cs.SSH.ListSSHKeyPairs(p) 644 | if err != nil { 645 | return err 646 | } 647 | if res.Count > 0 { 648 | return fmt.Errorf("SSH key pair (%v) already exists.", d.SSHKeyPair) 649 | } 650 | return nil 651 | } 652 | 653 | func (d *Driver) checkInstance() error { 654 | cs := d.getClient() 655 | 656 | log.Infof("Checking if instance (%v) already exists...", d.MachineName) 657 | 658 | p := cs.VirtualMachine.NewListVirtualMachinesParams() 659 | p.SetName(d.MachineName) 660 | p.SetZoneid(d.ZoneID) 661 | res, err := cs.VirtualMachine.ListVirtualMachines(p) 662 | if err != nil { 663 | return err 664 | } 665 | if res.Count > 0 { 666 | return fmt.Errorf("Instance (%v) already exists.", d.SSHKeyPair) 667 | } 668 | return nil 669 | } 670 | 671 | func (d *Driver) createKeyPair() error { 672 | cs := d.getClient() 673 | 674 | if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { 675 | return err 676 | } 677 | 678 | publicKey, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub") 679 | if err != nil { 680 | return err 681 | } 682 | 683 | log.Infof("Registering SSH key pair...") 684 | 685 | p := cs.SSH.NewRegisterSSHKeyPairParams(d.SSHKeyPair, string(publicKey)) 686 | if _, err := cs.SSH.RegisterSSHKeyPair(p); err != nil { 687 | return err 688 | } 689 | 690 | return nil 691 | } 692 | 693 | func (d *Driver) deleteKeyPair() error { 694 | cs := d.getClient() 695 | 696 | log.Infof("Deleting SSH key pair...") 697 | 698 | p := cs.SSH.NewDeleteSSHKeyPairParams(d.SSHKeyPair) 699 | if _, err := cs.SSH.DeleteSSHKeyPair(p); err != nil { 700 | return err 701 | } 702 | return nil 703 | } 704 | 705 | func (d *Driver) associatePublicIP() error { 706 | cs := d.getClient() 707 | log.Infof("Associating public ip address...") 708 | p := cs.Address.NewAssociateIpAddressParams() 709 | p.SetZoneid(d.ZoneID) 710 | if d.NetworkID != "" { 711 | p.SetNetworkid(d.NetworkID) 712 | } 713 | ip, err := cs.Address.AssociateIpAddress(p) 714 | if err != nil { 715 | return err 716 | } 717 | d.PublicIP = ip.Ipaddress 718 | d.PublicIPID = ip.Id 719 | d.DisassociatePublicIP = true 720 | 721 | return nil 722 | } 723 | 724 | func (d *Driver) disassociatePublicIP() error { 725 | if !d.DisassociatePublicIP { 726 | return nil 727 | } 728 | 729 | cs := d.getClient() 730 | log.Infof("Disassociating public ip address...") 731 | p := cs.Address.NewDisassociateIpAddressParams(d.PublicIPID) 732 | if _, err := cs.Address.DisassociateIpAddress(p); err != nil { 733 | return err 734 | } 735 | 736 | return nil 737 | } 738 | 739 | func (d *Driver) enableStaticNat() error { 740 | cs := d.getClient() 741 | log.Infof("Enabling Static Nat...") 742 | p := cs.NAT.NewEnableStaticNatParams(d.PublicIPID, d.Id) 743 | if _, err := cs.NAT.EnableStaticNat(p); err != nil { 744 | return err 745 | } 746 | 747 | return nil 748 | } 749 | 750 | func (d *Driver) configureFirewallRule(publicPort, privatePort int) error { 751 | cs := d.getClient() 752 | 753 | log.Debugf("Creating firewall rule ... : cidr list: %v, port %d", d.CIDRList, publicPort) 754 | p := cs.Firewall.NewCreateFirewallRuleParams(d.PublicIPID, "tcp") 755 | p.SetCidrlist(d.CIDRList) 756 | p.SetStartport(publicPort) 757 | p.SetEndport(publicPort) 758 | rule, err := cs.Firewall.CreateFirewallRule(p) 759 | if err != nil { 760 | // If the error reports the port is already open, just ignore. 761 | if !strings.Contains(err.Error(), fmt.Sprintf( 762 | "The range specified, %d-%d, conflicts with rule", publicPort, publicPort)) { 763 | return err 764 | } 765 | } else { 766 | d.FirewallRuleIds = append(d.FirewallRuleIds, rule.Id) 767 | } 768 | 769 | return nil 770 | } 771 | 772 | func (d *Driver) configurePortForwardingRule(publicPort, privatePort int) error { 773 | cs := d.getClient() 774 | 775 | log.Debugf("Creating port forwarding rule ... : cidr list: %v, port %d", d.CIDRList, publicPort) 776 | p := cs.Firewall.NewCreatePortForwardingRuleParams( 777 | d.PublicIPID, privatePort, "tcp", publicPort, d.Id) 778 | p.SetOpenfirewall(false) 779 | if _, err := cs.Firewall.CreatePortForwardingRule(p); err != nil { 780 | return err 781 | } 782 | 783 | return nil 784 | } 785 | 786 | func (d *Driver) configureFirewallRules() error { 787 | log.Info("Creating firewall rule for ssh port ...") 788 | 789 | if err := d.configureFirewallRule(22, 22); err != nil { 790 | return err 791 | } 792 | 793 | log.Info("Creating firewall rule for docker port ...") 794 | if err := d.configureFirewallRule(dockerPort, dockerPort); err != nil { 795 | return err 796 | } 797 | 798 | if d.SwarmMaster { 799 | log.Info("Creating firewall rule for swarm port ...") 800 | if err := d.configureFirewallRule(swarmPort, swarmPort); err != nil { 801 | return err 802 | } 803 | } 804 | 805 | return nil 806 | } 807 | 808 | func (d *Driver) deleteFirewallRules() error { 809 | if len(d.FirewallRuleIds) > 0 { 810 | log.Info("Removing firewall rules...") 811 | for _, id := range d.FirewallRuleIds { 812 | cs := d.getClient() 813 | f := cs.Firewall.NewDeleteFirewallRuleParams(id) 814 | if _, err := cs.Firewall.DeleteFirewallRule(f); err != nil { 815 | return err 816 | } 817 | } 818 | } 819 | return nil 820 | } 821 | 822 | func (d *Driver) configurePortForwardingRules() error { 823 | log.Info("Creating port forwarding rule for ssh port ...") 824 | 825 | if err := d.configurePortForwardingRule(22, 22); err != nil { 826 | return err 827 | } 828 | 829 | log.Info("Creating port forwarding rule for docker port ...") 830 | if err := d.configurePortForwardingRule(dockerPort, dockerPort); err != nil { 831 | return err 832 | } 833 | 834 | if d.SwarmMaster { 835 | log.Info("Creating port forwarding rule for swarm port ...") 836 | if err := d.configurePortForwardingRule(swarmPort, swarmPort); err != nil { 837 | return err 838 | } 839 | } 840 | 841 | return nil 842 | } 843 | 844 | func (d *Driver) createSecurityGroup() error { 845 | log.Debugf("Creating security group ...") 846 | cs := d.getClient() 847 | 848 | p1 := cs.SecurityGroup.NewCreateSecurityGroupParams(d.MachineName) 849 | if _, err := cs.SecurityGroup.CreateSecurityGroup(p1); err != nil { 850 | return err 851 | } 852 | 853 | p2 := cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() 854 | p2.SetSecuritygroupname(d.MachineName) 855 | p2.SetProtocol("tcp") 856 | p2.SetCidrlist(d.CIDRList) 857 | 858 | p2.SetStartport(22) 859 | p2.SetEndport(22) 860 | if _, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p2); err != nil { 861 | return err 862 | } 863 | 864 | p2.SetStartport(dockerPort) 865 | p2.SetEndport(dockerPort) 866 | if _, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p2); err != nil { 867 | return err 868 | } 869 | 870 | if d.SwarmMaster { 871 | p2.SetStartport(swarmPort) 872 | p2.SetEndport(swarmPort) 873 | if _, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p2); err != nil { 874 | return err 875 | } 876 | } 877 | return nil 878 | } 879 | 880 | func (d *Driver) deleteSecurityGroup() error { 881 | log.Debugf("Deleting security group ...") 882 | cs := d.getClient() 883 | 884 | p := cs.SecurityGroup.NewDeleteSecurityGroupParams() 885 | p.SetName(d.MachineName) 886 | if _, err := cs.SecurityGroup.DeleteSecurityGroup(p); err != nil { 887 | return err 888 | } 889 | return nil 890 | } 891 | -------------------------------------------------------------------------------- /cloudstack_test.go: -------------------------------------------------------------------------------- 1 | package cloudstack 2 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 8170c88c66e4380f8bf3b01cc7f09658b2572fe07670afa150124a7e5ea205a5 2 | updated: 2017-10-22T16:37:38.113790002+09:00 3 | imports: 4 | - name: github.com/Azure/go-ansiterm 5 | version: d6e3b3328b783f23731bc4d058875b0371ff8109 6 | - name: github.com/docker/docker 7 | version: a8a31eff10544860d2188dddabdee4d727545796 8 | subpackages: 9 | - pkg/term 10 | - name: github.com/docker/machine 11 | version: 0c3f538eb470d1546c42f82646000726e237c2b6 12 | subpackages: 13 | - libmachine/drivers 14 | - libmachine/drivers/plugin 15 | - libmachine/drivers/plugin/localbinary 16 | - libmachine/drivers/rpc 17 | - libmachine/log 18 | - libmachine/mcnflag 19 | - libmachine/mcnutils 20 | - libmachine/ssh 21 | - libmachine/state 22 | - libmachine/version 23 | - version 24 | - name: github.com/Sirupsen/logrus 25 | version: 89742aefa4b206dcf400792f3bd35b542998eb3b 26 | - name: github.com/xanzy/go-cloudstack 27 | version: c815114af88405d9a8464cab25b71af3b38547c9 28 | subpackages: 29 | - cloudstack 30 | - name: golang.org/x/crypto 31 | version: 51714a8c4ac1764f07ab4127d7f739351ced4759 32 | subpackages: 33 | - curve25519 34 | - ed25519 35 | - ed25519/internal/edwards25519 36 | - ssh 37 | - ssh/terminal 38 | - name: golang.org/x/sys 39 | version: d9157a9621b69ad1d8d77a1933590c416593f24f 40 | subpackages: 41 | - unix 42 | - windows/registry 43 | testImports: [] 44 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/atsaki/docker-machine-driver-cloudstack 2 | import: 3 | - package: github.com/docker/docker 4 | - package: golang.org/x/crypto 5 | - package: github.com/docker/machine 6 | - package: github.com/xanzy/go-cloudstack 7 | - package: github.com/Azure/go-ansiterm 8 | - package: github.com/Sirupsen/logrus 9 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package cloudstack 2 | 3 | const Version string = "v0.1.5" 4 | --------------------------------------------------------------------------------