diff --git a/README.md b/README.md index e80f2c4..cee0843 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/chef/knife/cloud/google_service.rb b/lib/chef/knife/cloud/google_service.rb index a344b25..70ba809 100644 --- a/lib/chef/knife/cloud/google_service.rb +++ b/lib/chef/knife/cloud/google_service.rb @@ -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 @@ -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 @@ -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] @@ -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) @@ -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 @@ -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 diff --git a/lib/chef/knife/google_server_create.rb b/lib/chef/knife/google_server_create.rb index b373961..c909efd 100644 --- a/lib/chef/knife/google_server_create.rb +++ b/lib/chef/knife/google_server_create.rb @@ -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", @@ -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?, diff --git a/spec/cloud/google_service_spec.rb b/spec/cloud/google_service_spec.rb index 1c76f39..f745183 100644 --- a/spec/cloud/google_service_spec.rb +++ b/spec/cloud/google_service_spec.rb @@ -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", @@ -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 @@ -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) @@ -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") @@ -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 @@ -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 @@ -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")