magellan-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +60 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +109 -0
  9. data/Rakefile +7 -0
  10. data/bin/magellan-cli +13 -0
  11. data/launch_options.json +7 -0
  12. data/lib/magellan/cli/base.rb +50 -0
  13. data/lib/magellan/cli/command.rb +29 -0
  14. data/lib/magellan/cli/direct.rb +32 -0
  15. data/lib/magellan/cli/errors.rb +14 -0
  16. data/lib/magellan/cli/http.rb +103 -0
  17. data/lib/magellan/cli/login.rb +134 -0
  18. data/lib/magellan/cli/resources/base.rb +136 -0
  19. data/lib/magellan/cli/resources/client_version.rb +15 -0
  20. data/lib/magellan/cli/resources/container_image.rb +13 -0
  21. data/lib/magellan/cli/resources/container_instance.rb +25 -0
  22. data/lib/magellan/cli/resources/function_unit.rb +27 -0
  23. data/lib/magellan/cli/resources/host_instance.rb +56 -0
  24. data/lib/magellan/cli/resources/project.rb +22 -0
  25. data/lib/magellan/cli/resources/stage.rb +69 -0
  26. data/lib/magellan/cli/resources/worker_version.rb +41 -0
  27. data/lib/magellan/cli/resources.rb +27 -0
  28. data/lib/magellan/cli/sample_launch_options.json +7 -0
  29. data/lib/magellan/cli/ssl.rb +35 -0
  30. data/lib/magellan/cli/version.rb +5 -0
  31. data/lib/magellan/cli.rb +23 -0
  32. data/magellan-cli.gemspec +29 -0
  33. data/spec/magellan/cli/Magellan.yml +29 -0
  34. data/spec/magellan/cli/login_page.html +42 -0
  35. data/spec/magellan/cli/login_spec.rb +46 -0
  36. data/spec/magellan/cli/resources/project_spec.rb +39 -0
  37. data/spec/magellan/cli_spec.rb +7 -0
  38. data/spec/spec_helper.rb +2 -0
  39. metadata +187 -0
@@ -0,0 +1,134 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli"
3
+
4
+ require 'httpclient'
5
+ require 'json'
6
+ require 'uri'
7
+ require 'nokogiri'
8
+
9
+ require 'active_support/core_ext/hash/keys'
10
+
11
+ module Magellan
12
+ module Cli
13
+ class Login
14
+
15
+ PRODUCTION_HTTP_PORT = 80
16
+ PRODUCTION_HTTPS_PORT = 443
17
+
18
+ DEFAULT_HTTP_PORT = (ENV['DEFAULT_HTTP_PORT' ] || 80).to_i
19
+ DEFAULT_HTTPS_PORT = (ENV['DEFAULT_HTTPS_PORT'] || 443).to_i
20
+
21
+ attr_reader :httpclient
22
+ attr_reader :base_http_url
23
+ attr_reader :base_https_url
24
+ attr_reader :ssl_disabled
25
+
26
+ attr_reader :auth_token
27
+
28
+ # Magellan::Cli::Loginのコンストラクタです。
29
+ #
30
+ # @param [String] base_url_or_host 接続先の基準となるURLあるいはホスト名
31
+ # @param [Hash] options オプション
32
+ # @option options [String] :api_version APIのバージョン。デフォルトは "1.0.0"
33
+ # @option options [Boolean] :ssl_disabled SSLを無効にするかどうか。
34
+ # @option options [Integer] :https_port HTTPSで接続する際の接続先のポート番号
35
+ def initialize(base_url_or_host = nil, options = {})
36
+ base_url_or_host ||= (ENV["MAGELLAN_SITE"] || "http://localhost:3000/")
37
+ if base_url_or_host =~ URI.regexp
38
+ @base_http_url = base_url_or_host.sub(/\/\Z/, '')
39
+ uri = URI.parse(@base_http_url)
40
+ else
41
+ if config_path = search_file(".magellan-cli.yml")
42
+ config = YAML.load_file_with_erb(config_path)
43
+ options = config[base_url_or_host.to_s].deep_symbolize_keys.update(options)
44
+ end
45
+ uri = URI::Generic.build({scheme: "http", host: base_url_or_host, port: DEFAULT_HTTP_PORT}.update(options))
46
+ @base_http_url = uri.to_s
47
+ end
48
+ @ssl_disabled = options.delete(:ssl_disabled)
49
+ @ssl_disabled ||= (uri.port == 3000)
50
+ @base_https_url = @ssl_disabled ? @base_http_url : build_https_url(uri, options[:https_port])
51
+ # @api_version = options[:api_version] || "1.0.0"
52
+
53
+ @httpclient = HTTPClient.new
54
+ @httpclient.ssl_config.verify_mode = nil # 自己署名の証明書をOKにする
55
+ end
56
+
57
+ def build_https_url(uri, port = nil)
58
+ uri.scheme = "https"
59
+ uri.port = port || (uri.port == PRODUCTION_HTTP_PORT ? PRODUCTION_HTTPS_PORT : uri.port + 1)
60
+ uri.to_s
61
+ end
62
+
63
+ def login_form_url
64
+ @login_form_url ||= base_https_url + "/users/sign_in.html"
65
+ end
66
+ def login_url
67
+ @login_url ||= base_https_url + "/api/sign_in.json"
68
+ end
69
+
70
+ # magellan-apiサーバに接続してログインの検証と処理を行います。
71
+ #
72
+ # @param [Hash] extra オプション
73
+ # @return [Integer] サーバが返したレスポンスのステータスを返します
74
+ # @return [String] サーバが返したレスポンスのbodyを返します
75
+ def login_and_status
76
+ @auth_token ||= get_auth_token
77
+ params = {
78
+ "user" => {"email" => "magellan@groovenauts.jp", "password" => "password"},
79
+ "authenticity_token" => @auth_token
80
+ }.to_json
81
+ res2 = Ssl.retry_on_ssl_error("login"){ @httpclient.post(login_url, params, JSON_HEADER) }
82
+ return res2.status, res2.body
83
+ end
84
+
85
+ # GSSサーバに接続してログインの検証と処理を行います。
86
+ #
87
+ # @param [Hash] extra オプション
88
+ # @option extra [Integer] :device_type デバイス種別
89
+ # @option extra [Integer] :device_id デバイス識別子
90
+ # @return [Boolean] ログインに成功した場合はtrue、失敗した場合はfalse
91
+ def login
92
+ status, _ = login_and_status
93
+ case status
94
+ when 200...300 then true
95
+ else false
96
+ end
97
+ end
98
+
99
+ # GSSサーバに接続してログインの検証と処理を行います。
100
+ #
101
+ # @param [Hash] extra オプション
102
+ # @see #login
103
+ # @return ログインに成功した場合は自身のオブジェクト返します。失敗した場合はLibgss::Network::Errorがraiseされます。
104
+ def login!
105
+ status, body = login_and_status
106
+ case status
107
+ when 200...300 then return self
108
+ else raise LoginError, "status: #{status} #{body}"
109
+ end
110
+ end
111
+
112
+ def get_auth_token
113
+ res = Ssl.retry_on_ssl_error("login_form"){ @httpclient.get(login_form_url) }
114
+ doc = Nokogiri::HTML.parse(res.body, login_form_url, res.body_encoding.to_s)
115
+ node = doc.xpath('//input[@name="authenticity_token"]').first
116
+ node.attribute('value').value
117
+ end
118
+
119
+ # @httpclient.inspectの戻り値の文字列が巨大なので、inspectで出力しないようにします。
120
+ def inspect
121
+ r = "#<#{self.class.name}:#{self.object_id} "
122
+ fields = (instance_variables - [:@httpclient]).map{|f| "#{f}=#{instance_variable_get(f).inspect}"}
123
+ r << fields.join(", ") << ">"
124
+ end
125
+
126
+ def search_file(basename)
127
+ dirs = [".", "./config", ENV['HOME']].join(",")
128
+ Dir["{#{dirs}}/#{basename}"].select{|path| File.readable?(path)}.first
129
+ end
130
+ private :search_file
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,136 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'active_support/core_ext/string/inflections'
7
+ require 'active_support/core_ext/class/attribute_accessors'
8
+ require 'active_support/core_ext/object/blank'
9
+
10
+ module Magellan
11
+ module Cli
12
+ module Resources
13
+
14
+ class NotSelected < StandardError
15
+ end
16
+
17
+ class NotFound < StandardError
18
+ end
19
+
20
+ class Base < ::Magellan::Cli::Http
21
+
22
+ no_commands do
23
+
24
+ def load_selections
25
+ File.readable?(".magellan-cli") ? YAML.load_file(".magellan-cli") : {}
26
+ end
27
+
28
+ def load_selection(name)
29
+ sel = load_selections
30
+ s = sel[name]
31
+ raise NotSelected, "No #{name} selected" unless s
32
+ return s
33
+ end
34
+
35
+ def update_selections(hash = nil)
36
+ sel = load_selections
37
+ sel.update(hash) if hash
38
+ yield(sel) if block_given?
39
+ open(".magellan-cli", "w") do |f|
40
+ YAML.dump(sel, f)
41
+ end
42
+ end
43
+
44
+ def build_query(hash)
45
+ @@no ||= 0
46
+ r = {}
47
+ hash.each do |key, value|
48
+ @@no += 1
49
+ r["f[#{key}][#{@@no}][o]"] = "is"
50
+ r["f[#{key}][#{@@no}][v]"] = value
51
+ end
52
+ return r
53
+ end
54
+
55
+ def default_query
56
+ sel = load_selections
57
+ q = {}
58
+ (self.class.resource_dependency || {}).each do |f, res|
59
+ r = sel[res]
60
+ raise NotSelected, "No #{f} selected" unless r
61
+ q[f] = r["id"]
62
+ end
63
+ return build_query(q)
64
+ end
65
+
66
+ DEFAULT_SELECTION_FIELDS = %w[id name].freeze
67
+
68
+ def update_first_result(name, path, query, fields = DEFAULT_SELECTION_FIELDS)
69
+ results = get_json(path, query)
70
+ raise NotFound, "#{name} not found for #{query.inspect}" if results.blank? || results.first.blank?
71
+ r = results.first
72
+ obj = fields.each_with_object({}) do |f, d|
73
+ d[f] = r[f]
74
+ end
75
+ update_selections(name => obj)
76
+ return r
77
+ end
78
+
79
+ def list
80
+ r = get_json("/admin/#{self.class.resource_name}.json", default_query)
81
+ $stdout.puts(JSON.pretty_generate(r) << "\nTotal: #{r.length}")
82
+ end
83
+
84
+ def show(id)
85
+ r = get_json("/admin/#{self.class.resource_name}/#{id}.json")
86
+ $stdout.puts(JSON.pretty_generate(r))
87
+ end
88
+
89
+ def select(name)
90
+ q = build_query("name" => name).update(default_query)
91
+ update_first_result(self.class.resource_name, "/admin/#{self.class.resource_name}.json", q)
92
+ end
93
+
94
+ def deselect
95
+ update_selections do |s|
96
+ s.delete(self.class.resource_name)
97
+ end
98
+ end
99
+
100
+ def self.inherited(klass)
101
+ base_name = klass.name.split(/::/).last
102
+ res_name = base_name.underscore
103
+ klass.module_eval <<-EOM
104
+ no_commands do
105
+ cattr_accessor :resource_name
106
+ cattr_accessor :resource_dependency
107
+ end
108
+
109
+ desc "list", "list #{base_name}"
110
+ def list
111
+ super
112
+ end
113
+
114
+ desc "show ID", "show #{base_name} specified by ID"
115
+ def show(id)
116
+ super(id)
117
+ end
118
+
119
+ desc "select NAME", "select #{base_name} by NAME"
120
+ def select(name)
121
+ super
122
+ end
123
+
124
+ desc "deselect", "deselect #{base_name}"
125
+ def deselect
126
+ super
127
+ end
128
+ EOM
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class ClientVersion < Base
9
+ self.resource_name = "client_version"
10
+ self.resource_dependency = {"stage" => "stage"}
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class ContainerImage < Base
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class ContainerInstance < Base
9
+ self.resource_name = "container_instance"
10
+ self.resource_dependency = {"host" => "host_instance"}
11
+
12
+ desc "start ID", "start container instance specified by ID"
13
+ def start(id)
14
+ post_json("/admin/container_instance/#{id}/status_control.json", {})
15
+ end
16
+
17
+ desc "stop ID", "stop container instance specified by ID"
18
+ def stop(id)
19
+ delete("/admin/container_instance/#{id}/status_control.json")
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class FunctionUnit < Base
9
+ self.resource_name = "function_unit"
10
+ self.resource_dependency = {"stage" => "stage-version"}
11
+
12
+ desc "create [ATTRIBUTES]", "create FunctionUnit with ATTRIBUTES (filepath or JSON text)"
13
+ def create(attrs = nil)
14
+ stage_id = load_selection("stage-version")
15
+ attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
16
+ params = {
17
+ "function_unit" => {
18
+ "stage_version_id" => stage_id,
19
+ }.update(attrs)
20
+ }
21
+ post_json("/admin/function_unit/new.js", params)
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class HostInstance < Base
9
+ self.resource_name = "host_instance"
10
+ self.resource_dependency = {"stage" => "stage-version"}
11
+
12
+ desc "sample_launch_options", "dump sample launch_options for create"
13
+ def sample_launch_options
14
+ $stdout.puts File.read(File.expand_path("../sample_launch_options.json", __FILE__))
15
+ end
16
+
17
+ desc "create NAME [LAUNCH_OPTIONS]", "create Host Instance"
18
+ def create(name, launch_options = nil)
19
+ stage_id = load_selection("stage-version")
20
+ launch_options = JSON.parse(File.readable?(launch_options) ? File.read(launch_options) : launch_options)
21
+
22
+ params = {
23
+ "host_instance" => {
24
+ "stage_id" => stage_id,
25
+ "name" => name,
26
+ "launch_options_yaml" => YAML.dump(launch_options),
27
+ }
28
+ }
29
+ post_json("/admin/host_instance/new.js", params)
30
+ end
31
+
32
+ desc "launch", "launch host instance"
33
+ def launch
34
+ id = load_selection(self.class.resource_name)["id"]
35
+ post_json("/admin/host_instance/#{id}/status_control.json", {})
36
+ end
37
+
38
+ desc "prepare", "prepare host instance"
39
+ def prepare
40
+ id = load_selection(self.class.resource_name)["id"]
41
+ put_json("/admin/host_instance/#{id}/status_control.json", {})
42
+ end
43
+
44
+ desc "stop", "stop host instance"
45
+ def stop
46
+ id = load_selection(self.class.resource_name)["id"]
47
+ delete("/admin/host_instance/#{id}/status_control.json")
48
+ end
49
+
50
+
51
+
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+
4
+ module Magellan
5
+ module Cli
6
+ module Resources
7
+
8
+ class Project < Base
9
+ self.resource_name = "project"
10
+ self.resource_dependency = nil
11
+
12
+ desc "update ATTRIBUTES", "update project with ATTRIBUTES"
13
+ def update(attrs)
14
+ s = load_selection("project")
15
+ attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
16
+ put_json("/admin/magellan~auth~project/#{s['id']}/edit", {"magellan_auth_project" => attrs})
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan/cli/resources"
3
+ require 'cgi'
4
+
5
+ module Magellan
6
+ module Cli
7
+ module Resources
8
+
9
+ class Stage < Base
10
+ self.resource_name = "stage~title"
11
+ self.resource_dependency = {"project" => "project"}
12
+
13
+ desc "select NAME", "select Stage named by NAME"
14
+ def select(name)
15
+ q = build_query("name" => name).update(default_query)
16
+ r = update_first_result("stage", "/admin/stage~title.json", q)
17
+
18
+ # # current
19
+ # q = build_query("title" => r["id"], "phase" => 2) # 2: current
20
+ # update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
21
+
22
+ # # workspace
23
+ q = build_query("title" => r["id"], "phase" => 1) # 1: workspace
24
+ update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
25
+ end
26
+
27
+ desc "planning", "switch to planning to build next released version"
28
+ def planning
29
+ switch_version(1)
30
+ end
31
+
32
+ desc "current", "switch to current released version"
33
+ def current
34
+ switch_version(2)
35
+ end
36
+
37
+ no_commands do
38
+ def switch_version(phase)
39
+ s = load_selection("stage")
40
+ q = build_query("title" => s["id"], "phase" => phase) # 1: workspace, 2: current, 3: used
41
+ update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
42
+ end
43
+ end
44
+
45
+ desc "prepare", "prepare containers"
46
+ def prepare
47
+ s = load_selection("stage")
48
+ id = s["id"]
49
+ post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "prepare_containers"})
50
+ end
51
+
52
+ desc "update ATTRIBUTES", "update stage with ATTRIBUTES"
53
+ def update(attrs)
54
+ s = load_selection("stage")
55
+ attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
56
+ put_json("/admin/stage~title/#{s['id']}/edit", {"stage_title" => attrs})
57
+ end
58
+
59
+ desc "release_now", "release changes now"
60
+ def release_now
61
+ s = load_selection("stage")
62
+ id = s["id"]
63
+ post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "release_now"})
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end