magellan-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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