Skip to content

Commit

Permalink
Merge pull request #103 from chef/adamleff/subnets
Browse files Browse the repository at this point in the history
Add support for deploying instance on subnetworks
  • Loading branch information
adamleff committed Mar 15, 2016
2 parents d5930a7 + 61050cb commit 569bdae
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -185,6 +185,7 @@ See the [SSH Keys](#ssh-keys) section for more information.
* **INSTANCE_NAME**: required. The name to use when creating the instance.
* **--gce-machine-type**: required. The machine type to use when creating the server, such as `n1-standard-2` or `n1-highcpu-2-d`.
* **--gce-network**: The name of the network to which your instance will be attached. Defaults to "default".
* **--gce-subnet**: The name of the subnet to which your instance will be attached. Only applies to custom networks.
* **--gce-image**: required. The name of the disk image to use when creating the server. knife-google will search your current project for this disk image. If the image cannot be found but looks like a common public image, the public image project will be searched as well.
* Example: if you supply a gce-image of `centos-7-v20160219`, knife-google will first look for an image with that name in your currently-configured project. If it cannot be found, it will look in the `centos-cloud` project.
* This behavior can be overridden with the `--gce-image-project` parameter.
Expand Down
19 changes: 17 additions & 2 deletions lib/chef/knife/cloud/google_service.rb
Expand Up @@ -186,11 +186,12 @@ def list_project_quotas
def validate_server_create_options!(options)
raise "Invalid machine type: #{options[:machine_type]}" unless valid_machine_type?(options[:machine_type])
raise "Invalid network: #{options[:network]}" unless valid_network?(options[:network])
raise "Invalid subnet: #{options[:subnet]}" if options[:subnet] && !valid_subnet?(options[:subnet])
raise "Invalid Public IP setting: #{options[:public_ip]}" unless valid_public_ip_setting?(options[:public_ip])
raise "Invalid image: #{options[:image]} - check your image name, or set an image project if needed" if image_search_for(options[:image], options[:image_project]).nil?
end

def check_api_call(&block)
def check_api_call
yield
rescue Google::Apis::ClientError
false
Expand All @@ -208,6 +209,11 @@ def valid_network?(network)
check_api_call { connection.get_network(project, network) }
end

def valid_subnet?(subnet)
return false if subnet.nil?
check_api_call { connection.get_subnetwork(project, region, subnet) }
end

def image_exist?(image_project, image_name)
check_api_call { connection.get_image(image_project, image_name) }
end
Expand All @@ -232,6 +238,10 @@ def valid_ip_address?(ip_address)
true
end

def region
@region ||= connection.get_zone(project, zone).region.split("/").last
end

def instance_object_for(options)
inst_obj = Google::Apis::ComputeV1::Instance.new
inst_obj.name = options[:name]
Expand Down Expand Up @@ -331,6 +341,7 @@ def instance_metadata_for(metadata)
def instance_network_interfaces_for(options)
interface = Google::Apis::ComputeV1::NetworkInterface.new
interface.network = network_url_for(options[:network])
interface.subnetwork = subnet_url_for(options[:subnet]) if options[:subnet]
interface.access_configs = instance_access_configs_for(options[:public_ip])

Array(interface)
Expand All @@ -351,6 +362,10 @@ def network_url_for(network)
"projects/#{project}/global/networks/#{network}"
end

def subnet_url_for(subnet)
"projects/#{project}/regions/#{region}/subnetworks/#{subnet}"
end

def instance_scheduling_for(options)
scheduling = Google::Apis::ComputeV1::Scheduling.new
scheduling.automatic_restart = options[:auto_restart].to_s
Expand Down Expand Up @@ -467,7 +482,7 @@ def paginated_results(api_method, items_method, *args)
items
end

def wait_for_status(requested_status, &block)
def wait_for_status(requested_status)
last_status = ""

begin
Expand Down
5 changes: 5 additions & 0 deletions lib/chef/knife/google_server_create.rb
Expand Up @@ -102,6 +102,10 @@ class GoogleServerCreate < ServerCreateCommand
description: "The network for this server; default is 'default'",
default: "default"

option :subnet,
long: "--gce-subnet SUBNET",
description: "The name of the subnet in the network on which to deploy the instance"

option :tags,
short: "-T TAG1,TAG2,TAG3",
long: "--gce-tags TAG1,TAG2,TAG3",
Expand Down Expand Up @@ -153,6 +157,7 @@ def before_exec_command
image: locate_config_value(:image),
image_project: locate_config_value(:image_project),
network: locate_config_value(:network),
subnet: locate_config_value(:subnet),
public_ip: locate_config_value(:public_ip),
auto_migrate: auto_migrate?,
auto_restart: auto_restart?,
Expand Down
80 changes: 74 additions & 6 deletions spec/cloud/google_service_spec.rb
Expand Up @@ -189,6 +189,7 @@
{
machine_type: "test_type",
network: "test_network",
subnet: "test_subnet",
public_ip: "public_ip",
image: "test_image",
image_project: "test_image_project",
Expand All @@ -198,6 +199,7 @@
before do
allow(service).to receive(:valid_machine_type?).and_return(true)
allow(service).to receive(:valid_network?).and_return(true)
allow(service).to receive(:valid_subnet?).and_return(true)
allow(service).to receive(:valid_public_ip_setting?).and_return(true)
allow(service).to receive(:image_search_for).and_return(true)
end
Expand All @@ -216,6 +218,11 @@
expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
end

it "raises an exception if the network is not valid" do
expect(service).to receive(:valid_subnet?).with("test_subnet").and_return(false)
expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
end

it "raises an exception if the public ip setting is not valid" do
expect(service).to receive(:valid_public_ip_setting?).with("public_ip").and_return(false)
expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
Expand Down Expand Up @@ -267,6 +274,20 @@
end
end

describe '#valid_subnet?' do
it "returns false if no subnet was specified" do
expect(service.valid_subnet?(nil)).to eq(false)
end

it "checks the network using check_api_call" do
expect(service).to receive(:region).and_return("test_region")
expect(connection).to receive(:get_subnetwork).with(project, "test_region", "test_subnet")
expect(service).to receive(:check_api_call).and_call_original

service.valid_subnet?("test_subnet")
end
end

describe '#image_exist?' do
it "checks the image using check_api_call" do
expect(connection).to receive(:get_image).with("image_project", "image_name")
Expand Down Expand Up @@ -312,6 +333,14 @@
end
end

describe '#region' do
it "returns the region for a given zone" do
zone_obj = double("zone_obj", region: "/path/to/test_region")
expect(connection).to receive(:get_zone).with(project, zone).and_return(zone_obj)
expect(service.region).to eq("test_region")
end
end

describe '#instance_object_for' do
let(:instance_object) { double("instance_object") }
let(:options) do
Expand Down Expand Up @@ -543,18 +572,50 @@
end

describe '#instance_network_interfaces_for' do
it "returns an array containing a properly-formatted interface" do
interface = double("interface")
options = { network: "test_network", public_ip: "public_ip" }
let(:interface) { double("interface" ) }
let(:options) { { network: "test_network", public_ip: "public_ip" } }

expect(service).to receive(:network_url_for).with("test_network").and_return("network_url")
expect(service).to receive(:instance_access_configs_for).with("public_ip").and_return("access_configs")
before do
allow(service).to receive(:network_url_for)
allow(service).to receive(:subnet_url_for)
allow(service).to receive(:instance_access_configs_for)
allow(Google::Apis::ComputeV1::NetworkInterface).to receive(:new).and_return(interface)
allow(interface).to receive(:network=)
allow(interface).to receive(:subnetwork=)
allow(interface).to receive(:access_configs=)
end

it "creates a network interface object and returns it" do
expect(Google::Apis::ComputeV1::NetworkInterface).to receive(:new).and_return(interface)
expect(service.instance_network_interfaces_for(options)).to eq([interface])
end

it "sets the network" do
expect(service).to receive(:network_url_for).with("test_network").and_return("network_url")
expect(interface).to receive(:network=).with("network_url")
service.instance_network_interfaces_for(options)
end

it "sets the access configs" do
expect(service).to receive(:instance_access_configs_for).with("public_ip").and_return("access_configs")
expect(interface).to receive(:access_configs=).with("access_configs")
service.instance_network_interfaces_for(options)
end

expect(service.instance_network_interfaces_for(options)).to eq([interface])
it "does not set a subnetwork" do
expect(service).not_to receive(:subnet_url_for)
expect(interface).not_to receive(:subnetwork=)
service.instance_network_interfaces_for(options)
end

context "when a subnet exists" do
let(:options) { { network: "test_network", subnet: "test_subnet", public_ip: "public_ip" } }

it "sets the subnetwork" do
expect(service).to receive(:subnet_url_for).with("test_subnet").and_return("subnet_url")
expect(interface).to receive(:subnetwork=).with("subnet_url")
service.instance_network_interfaces_for(options)
end
end
end

Expand All @@ -564,6 +625,13 @@
end
end

describe '#subnet_url_for' do
it "returns a properly-formatted subnet URL" do
expect(service).to receive(:region).and_return("test_region")
expect(service.subnet_url_for("test_subnet")).to eq("projects/test_project/regions/test_region/subnetworks/test_subnet")
end
end

describe '#instance_scheduling_for' do
it "returns a properly-formatted scheduling object" do
scheduling = double("scheduling")
Expand Down

0 comments on commit 569bdae

Please sign in to comment.