├── gcp
├── __init__.py
├── gae.py
├── compute.py
├── gke.py
└── sql.py
├── model
├── __init__.py
├── gkenoodespoolsmodel.py
├── policymodel.py
└── schedulesmodel.py
├── tasks
├── __init__.py
├── schedule_tasks.py
└── policy_tasks.py
├── util
├── __init__.py
├── tz.py
├── utils.py
└── gcp.py
├── iam.png
├── .gcloudignore
├── Zorya_policies.png
├── Zorya_schedule.png
├── client
├── public
│ ├── favicon.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── assets
│ │ └── zorya.png
│ ├── index.js
│ ├── modules
│ │ ├── utils
│ │ │ ├── policy.js
│ │ │ └── schedule.js
│ │ ├── components
│ │ │ ├── AppPageActions.js
│ │ │ ├── AppPageContent.js
│ │ │ ├── ErrorAlert.js
│ │ │ ├── ScheduleTimeZone.js
│ │ │ ├── PolicyTags.js
│ │ │ ├── AppFrame.js
│ │ │ └── ScheduleTimeTable.js
│ │ └── api
│ │ │ ├── policy.js
│ │ │ └── schedule.js
│ ├── withProps.js
│ ├── pages
│ │ ├── NotFound
│ │ │ └── NotFound.js
│ │ ├── AppIndex.js
│ │ ├── Schedule
│ │ │ ├── ScheduleEdit.js
│ │ │ ├── ScheduleCreate.js
│ │ │ └── ScheduleList.js
│ │ └── Policy
│ │ │ ├── PolicyList.js
│ │ │ └── Policy.js
│ └── withRoot.js
├── .prettierrc
├── README.md
├── .eslintrc
├── .gitignore
└── package.json
├── App_Engine_Flex_Tagging_Example.png
├── cron.yaml
├── appengine_config.py
├── docs
├── policy_example.json
├── api_curl.txt
└── schedule_example.json
├── requirements.txt
├── .gitattributes
├── App Engine Flex Tagging.md
├── deploy.sh
├── LICENSE
├── app.yaml
├── .gitignore
├── README.md
└── main.py
/gcp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/model/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tasks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/iam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/iam.png
--------------------------------------------------------------------------------
/.gcloudignore:
--------------------------------------------------------------------------------
1 |
2 | .gcloudignore
3 | .git
4 | .gitignore
5 | client
6 |
7 |
--------------------------------------------------------------------------------
/Zorya_policies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/Zorya_policies.png
--------------------------------------------------------------------------------
/Zorya_schedule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/Zorya_schedule.png
--------------------------------------------------------------------------------
/client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/client/public/favicon.png
--------------------------------------------------------------------------------
/client/src/assets/zorya.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/client/src/assets/zorya.png
--------------------------------------------------------------------------------
/App_Engine_Flex_Tagging_Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doitintl/zorya/HEAD/App_Engine_Flex_Tagging_Example.png
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "tabWidth": 2,
5 | "printWidth": 80
6 | }
7 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | ## Zorya web client
2 |
3 | ### development
4 | yarn start
5 |
6 | assumes backend is running on http://localhost:8080
--------------------------------------------------------------------------------
/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": "error"
6 | }
7 | }
--------------------------------------------------------------------------------
/cron.yaml:
--------------------------------------------------------------------------------
1 | cron:
2 | - description: "Run scheduled tasks"
3 | url: /tasks/schedule
4 | schedule: every 60 minutes synchronized
5 | target: default
6 |
--------------------------------------------------------------------------------
/appengine_config.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import vendor
2 | import sys
3 | # Add any libraries installed in the "lib" folder.
4 | vendor.add('lib')
5 | sys.path = ['lib'] + sys.path
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Index from './pages/AppIndex';
4 |
5 | ReactDOM.render(, document.querySelector('#root'));
6 |
--------------------------------------------------------------------------------
/client/src/modules/utils/policy.js:
--------------------------------------------------------------------------------
1 | export const getDefaultPolicy = () => ({
2 | name: '',
3 |
4 | displayname: '',
5 |
6 | // array of strings
7 | projects: [],
8 |
9 | // array of objects
10 | tags: [],
11 |
12 | schedulename: '',
13 | });
14 |
--------------------------------------------------------------------------------
/docs/policy_example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My policy",
3 | "tags": [
4 | {
5 | "dev": "sleeper",
6 | "staging": "resting"
7 | }
8 | ],
9 | "projetcs": [
10 | "project-x",
11 | "y-project"
12 | ],
13 | "schedulename": "my schedule name"
14 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.1.2
2 | Werkzeug==1.0.1
3 | google-api-python-client
4 | google-auth
5 | google-auth-httplib2
6 | pytz
7 | tzlocal
8 | backoff==1.10.0
9 | google-cloud-ndb==1.8.0
10 | google-cloud-tasks==1.5.0
11 | google-cloud-logging
12 | googleapis-common-protos==1.53.0
13 | numpy
--------------------------------------------------------------------------------
/model/gkenoodespoolsmodel.py:
--------------------------------------------------------------------------------
1 | """DB model GKE node pools."""
2 | from google.cloud import ndb
3 |
4 |
5 | class GkeNodePoolModel(ndb.Model):
6 | """Class that represents Number of nodes in a nodepool."""
7 |
8 | Name = ndb.StringProperty(indexed=True, required=True)
9 | NumberOfNodes = ndb.IntegerProperty(required=True)
10 |
--------------------------------------------------------------------------------
/client/src/withProps.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const withProps = (Component, componentProps) => {
4 | return class Route extends React.Component {
5 | render() {
6 | let props = Object.assign({}, this.props, componentProps);
7 | return ;
8 | }
9 | };
10 | };
11 |
12 | export default withProps;
13 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Zorya",
3 | "name": "Zorya",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/model/policymodel.py:
--------------------------------------------------------------------------------
1 | """DB model for policy."""
2 | from google.cloud import ndb
3 |
4 |
5 | class PolicyModel(ndb.Model):
6 | """Class that represents a tags and their associated schedule."""
7 | Name = ndb.StringProperty(indexed=True, required=True)
8 | DisplayName = ndb.JsonProperty(indexed=True)
9 | Tags = ndb.JsonProperty(repeated=True)
10 | Projects = ndb.JsonProperty(repeated=True)
11 | Schedule = ndb.StringProperty(indexed=True, required=True, repeated=False)
12 |
--------------------------------------------------------------------------------
/model/schedulesmodel.py:
--------------------------------------------------------------------------------
1 | """Database representation of schedule."""
2 |
3 | from google.cloud import ndb
4 |
5 | from util import tz
6 |
7 |
8 | class SchedulesModel(ndb.Model):
9 | """Stores scheduling data."""
10 | Name = ndb.StringProperty(indexed=True, required=True)
11 | DisplayName = ndb.StringProperty(indexed=True)
12 | Timezone = ndb.StringProperty(
13 | default='UTC', choices=tz.get_all_timezones(), required=True)
14 | Schedule = ndb.JsonProperty(required=True)
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | Basic .gitattributes for a python repo.
2 |
3 | # Source files
4 | # ============
5 | *.pxd text
6 | *.py text
7 | *.py3 text
8 | *.pyw text
9 | *.pyx text
10 |
11 | # Binary files
12 | # ============
13 | *.db binary
14 | *.p binary
15 | *.pkl binary
16 | *.pyc binary
17 | *.pyd binary
18 | *.pyo binary
19 |
20 | # Note: .db, .p, and .pkl files are associated
21 | # with the python modules ``pickle``, ``dbm.*``,
22 | # ``shelve``, ``marshal``, ``anydbm``, & ``bsddb``
23 | # (among others).
24 | static/* linguist-vendored
25 | templates/* linguist-vendored
26 |
--------------------------------------------------------------------------------
/App Engine Flex Tagging.md:
--------------------------------------------------------------------------------
1 | # App Engine Flex Tagging
2 |
3 | This file describe how to add an App Engine Flex resource to Zorya policy.
4 |
5 | In order to add GAE Flex to the policy it needed to take the two following steps:
6 |
7 | 1. Use the reserved word @app_engine_flex on the tag key.
8 | 2. Enter the GAE flex service-id and the version id to the tag value, with a delimiter ":".
9 |
10 | Example of tagging GAE flex:
11 |
12 | 
13 |
14 | Please note, unlike from the other resources that Zorya support, there is no need to add a label on the GAE service.
--------------------------------------------------------------------------------
/client/src/pages/NotFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Material UI
4 | import { withStyles } from '@material-ui/core/styles';
5 | import Typography from '@material-ui/core/Typography';
6 |
7 | const styles = (theme) => ({
8 | root: {
9 | padding: theme.spacing(1),
10 | },
11 | });
12 |
13 | class NotFound extends React.Component {
14 | render() {
15 | const { classes, location } = this.props;
16 |
17 | return (
18 |
19 |
20 | Not Found: {location.pathname}
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | export default withStyles(styles)(NotFound);
28 |
--------------------------------------------------------------------------------
/client/src/modules/utils/schedule.js:
--------------------------------------------------------------------------------
1 | export const getDefaultSchedule = () => ({
2 | name: '',
3 | displayname: '',
4 | timezone: 'UTC',
5 | Shape: [7, 24],
6 | dtype: 'int64',
7 | Corder: true,
8 | __ndarray__: [
9 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
10 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
11 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
12 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
13 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
14 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
15 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
16 | ],
17 | });
18 |
--------------------------------------------------------------------------------
/client/src/modules/components/AppPageActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Material UI
4 | import { withStyles } from '@material-ui/core/styles';
5 | import Divider from '@material-ui/core/Divider';
6 | import AppBar from '@material-ui/core/AppBar';
7 | import Toolbar from '@material-ui/core/Toolbar';
8 |
9 | const styles = (theme) => ({
10 | appBar: {
11 | backgroundColor: theme.palette.background.default,
12 | },
13 | });
14 |
15 | class AppPageActions extends React.Component {
16 | render() {
17 | const { classes, children } = this.props;
18 |
19 | return (
20 |
21 |
28 | {children}
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default withStyles(styles)(AppPageActions);
37 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ $# -eq 0 ]] ; then
4 | echo Missing project id argument
5 | exit
6 | fi
7 |
8 | PROJECTID=`gcloud projects list | grep -iw "$1" | awk '{print $1}'`
9 |
10 | if [ -z "$PROJECTID" ]; then
11 | echo Project $1 Not Found!
12 | exit
13 | fi
14 |
15 | echo Project ID $PROJECTID
16 | gcloud config set project $PROJECTID
17 |
18 | rm -rf ./build
19 | cd client && yarn install && yarn build && cd ..
20 | ##Build the task queue
21 |
22 |
23 | if gcloud tasks queues list|grep zorya-tasks >/dev/null 2>&1; then
24 | echo "Task Queue all ready exists"
25 | else
26 | gcloud tasks queues create zorya-tasks
27 | fi
28 | loc=`gcloud tasks queues describe zorya-tasks|grep name`
29 | LOCATION="$(echo $loc | cut -d'/' -f4)"
30 | echo
31 |
32 | file_location='util/location.py'
33 | if [ -f "$file_location" ]; then
34 | rm $file_location
35 | fi
36 | cat > $file_location <
24 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | return WithRoot;
34 | }
35 |
36 | export default withRoot;
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 DoiT International
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | # This file specifies your Python application's runtime configuration
2 | # including URL routing, versions, static file uploads, etc. See
3 | # https://developers.google.com/appengine/docs/python/config/appconfig
4 | # for details.
5 |
6 | runtime: python37
7 | service: default
8 |
9 | # Handlers define how to route requests to your application.
10 | handlers:
11 | - url: /api/v1/(.*)
12 | script: auto
13 | secure: always
14 |
15 | - url: /tasks/(.*)
16 | script: auto
17 | secure: always
18 |
19 | - url: /
20 | static_files: build/index.html
21 | upload: build/index.html
22 |
23 | - url: /favicon\.png
24 | static_files: build/favicon.png
25 | upload: build/favicon\.png
26 |
27 | # unused for now
28 | # - url: /service-worker\.js
29 | # static_files: build/service-worker.js
30 | # upload: build/service-worker\.js
31 |
32 | - url: /manifest\.json
33 | static_files: build/manifest.json
34 | upload: build/manifest\.json
35 |
36 | - url: /static/(.*)
37 | static_files: build/static/\1
38 | upload: build/static/(.*)
39 |
40 | - url: .*
41 | static_files: build/index.html
42 | upload: build/index.html
43 |
44 |
45 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zorya-web-client",
3 | "version": "1.0.0",
4 | "private": true,
5 | "proxy": "http://localhost:8080/",
6 | "dependencies": {
7 | "@material-ui/core": "^4.11.4",
8 | "@material-ui/icons": "^4.11.2",
9 | "classnames": "^2.3.1",
10 | "downshift": "^6.1.3",
11 | "lodash": "^4.17.19",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "react-lineto": "^3.2.0",
15 | "react-recompose": "^0.31.1",
16 | "react-router-dom": "^5.2.0",
17 | "react-scripts": "^4.0.3"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build && mv ./build ../build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | },
25 | "devDependencies": {
26 | "eslint-plugin-prettier": "^3.4.0",
27 | "prettier": "^2.3.0"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/util/tz.py:
--------------------------------------------------------------------------------
1 | """Time zone utilities."""
2 | from datetime import datetime
3 |
4 | import numpy as np
5 | import pytz
6 | import tzlocal
7 |
8 |
9 | def get_all_timezones():
10 | """Get a list of all timezones."""
11 | return pytz.all_timezones
12 |
13 |
14 | def get_local_timezone():
15 | """
16 | Get the local timezone.
17 | Returns: local time zone.
18 |
19 | """
20 | return tzlocal.get_localzone()
21 |
22 |
23 | def get_time_at_timezone(timezone):
24 | """
25 | Get the current time a a time zone.
26 | Args:
27 | timezone:
28 |
29 | Returns: time at the requestd timezone
30 |
31 | """
32 | tz = pytz.timezone(timezone)
33 | now = datetime.now(tz=tz)
34 | target_tz = pytz.timezone(timezone)
35 | return target_tz.normalize(now.astimezone(target_tz))
36 |
37 |
38 | def convert_time_to_index(time):
39 | """
40 | Convert a time to and index in an 7x24 array.
41 | Args:
42 | time:
43 |
44 | Returns: x,y of the index
45 |
46 | """
47 | days = np.arange(0, 7)
48 | days = np.roll(days, 1)
49 | for index, item in enumerate(days):
50 | if item == time.weekday():
51 | day = index
52 | hour = time.hour
53 | return day, hour
54 |
--------------------------------------------------------------------------------
/tasks/schedule_tasks.py:
--------------------------------------------------------------------------------
1 | """Change a state for all matching instances in a project."""
2 | import logging
3 |
4 | from gcp.compute import Compute
5 | from gcp.sql import Sql
6 | from gcp.gke import Gke
7 | from gcp.gae import Gae
8 |
9 |
10 | def change_state(tagkey, tagvalue, action, project):
11 | """
12 | Change a state for all matching instances in a project.
13 | Args:
14 | tagkey: tag key
15 | tagvalue: tag value
16 | action: stop 0 start 1
17 | project: project id
18 |
19 | Returns:
20 |
21 | """
22 | if not (check_if_app_engine_job(tagkey, tagvalue)):
23 | compute = Compute(project)
24 | sql = Sql(project)
25 | gke = Gke(project)
26 | logging.info("change_state %s action %s", project, action)
27 | compute.change_status(action, tagkey, tagvalue)
28 | sql.change_status(action, tagkey, tagvalue)
29 | gke.change_status(action, tagkey, tagvalue)
30 | else:
31 | service_id, version_id = get_service_and_version_from_tag_value(tagvalue)
32 | app_engine = Gae(project)
33 | logging.info("change_state %s action %s", project, action)
34 | app_engine.change_status(action, service_id, version_id)
35 | return 'ok', 200
36 |
37 |
38 | def check_if_app_engine_job(tagkey, tagvalue):
39 | """
40 | Infer by the tag key and value if its an App Engine job
41 |
42 | Args:
43 | tagkey: tag key
44 | tagvalue: tag value
45 |
46 | Returns:
47 | """
48 |
49 | if (tagkey == '@app_engine_flex') and (':' in tagvalue):
50 | return True
51 | else:
52 | return False
53 |
54 |
55 | def get_service_and_version_from_tag_value(tagvalue):
56 | return tagvalue.split(':')
57 |
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | util/location.py
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 | .idea
103 | config.json
104 |
105 | # node
106 | node_modules/
107 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Zorya
23 |
24 |
25 |
26 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/client/src/modules/components/AppPageContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Material UI
5 | import { withStyles } from '@material-ui/core/styles';
6 | import CircularProgress from '@material-ui/core/CircularProgress';
7 |
8 | // Project
9 | import ErrorAlert from './ErrorAlert';
10 |
11 | const styles = (theme) => ({
12 | root: {
13 | overflow: 'auto',
14 | padding: theme.spacing(2),
15 | height: 'calc(100% - 56px)',
16 | [theme.breakpoints.up('sm')]: {
17 | height: 'calc(100% - 64px)',
18 | },
19 | },
20 | centered: {
21 | position: 'absolute',
22 | left: '50%',
23 | top: '50%',
24 | },
25 | });
26 |
27 | class AppPageContent extends React.Component {
28 | render() {
29 | const { classes, children } = this.props;
30 |
31 | const {
32 | showBackendError,
33 | showLoadingSpinner,
34 | backendErrorTitle = 'An Error Occurred:',
35 | backendErrorMessage = 'Unspecified error, check logs.',
36 | onBackendErrorClose,
37 | } = this.props;
38 |
39 | return (
40 |
41 | {showLoadingSpinner ? (
42 |
43 | ) : (
44 | children
45 | )}
46 |
52 |
53 | );
54 | }
55 | }
56 |
57 | AppPageContent.propTypes = {
58 | showBackendError: PropTypes.bool.isRequired,
59 | showLoadingSpinner: PropTypes.bool,
60 | backendErrorTitle: PropTypes.string,
61 | backendErrorMessage: PropTypes.string,
62 | onBackendErrorClose: PropTypes.func.isRequired,
63 | };
64 |
65 | export default withStyles(styles)(AppPageContent);
66 |
--------------------------------------------------------------------------------
/client/src/modules/api/policy.js:
--------------------------------------------------------------------------------
1 | class PolicyService {
2 | list = async () => {
3 | const response = await fetch(`/api/v1/list_policies?verbose=true`, {
4 | method: 'GET',
5 | credentials: 'same-origin',
6 | });
7 |
8 | if (!response.ok) {
9 | console.error(response);
10 | const responseBody = await response.text();
11 | throw Error(responseBody || response.statusText);
12 | }
13 |
14 | return response.json();
15 | };
16 |
17 | get = async (policy) => {
18 | const response = await fetch(`/api/v1/get_policy?policy=${policy}`, {
19 | method: 'GET',
20 | credentials: 'same-origin',
21 | });
22 |
23 | if (!response.ok) {
24 | console.error(response);
25 | const responseBody = await response.text();
26 | throw Error(responseBody || response.statusText);
27 | }
28 |
29 | return response.json();
30 | };
31 |
32 | delete = async (policy) => {
33 | const response = await fetch(`/api/v1/del_policy?policy=${policy}`, {
34 | method: 'GET',
35 | credentials: 'same-origin',
36 | });
37 |
38 | if (!response.ok) {
39 | console.error(response);
40 | const responseBody = await response.text();
41 | throw Error(responseBody || response.statusText);
42 | }
43 |
44 | return response;
45 | };
46 |
47 | add = async (policy) => {
48 | const response = await fetch(`/api/v1/add_policy`, {
49 | method: 'POST',
50 | credentials: 'same-origin',
51 | headers: {
52 | 'Content-Type': 'application/json',
53 | },
54 | body: JSON.stringify(policy),
55 | });
56 |
57 | if (!response.ok) {
58 | console.error(response);
59 | const responseBody = await response.text();
60 | throw Error(responseBody || response.statusText);
61 | }
62 |
63 | return response;
64 | };
65 | }
66 |
67 | export default PolicyService;
68 |
--------------------------------------------------------------------------------
/client/src/modules/components/ErrorAlert.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Material UI
5 | import { withStyles } from '@material-ui/core/styles';
6 | import Button from '@material-ui/core/Button';
7 | import Dialog from '@material-ui/core/Dialog';
8 | import DialogActions from '@material-ui/core/DialogActions';
9 | import DialogContent from '@material-ui/core/DialogContent';
10 | import DialogContentText from '@material-ui/core/DialogContentText';
11 | import DialogTitle from '@material-ui/core/DialogTitle';
12 |
13 | const styles = (theme) => ({
14 | root: {
15 | height: '100%',
16 | },
17 | button: {
18 | marginRight: theme.spacing(2),
19 | },
20 | });
21 |
22 | class ErrorAlert extends React.Component {
23 | constructor(props, context) {
24 | super(props, context);
25 | this.onClose = props.onClose;
26 | }
27 |
28 | render() {
29 | const { classes, showError, errorTitle, errorMessage } = this.props;
30 |
31 | return (
32 |
33 |
51 |
52 | );
53 | }
54 | }
55 | ErrorAlert.propTypes = {
56 | showError: PropTypes.bool.isRequired,
57 | errorTitle: PropTypes.string,
58 | errorMessage: PropTypes.string,
59 | onClose: PropTypes.func.isRequired,
60 | };
61 |
62 | export default withStyles(styles)(ErrorAlert);
63 |
--------------------------------------------------------------------------------
/util/utils.py:
--------------------------------------------------------------------------------
1 | """Misc utils."""
2 | import json
3 | import logging
4 | import os
5 |
6 |
7 | def detect_gae():
8 | """Determine whether or not we're running on GAE.
9 |
10 | This is based on:
11 | https://developers.google.com/appengine/docs/python/#The_Environment
12 |
13 | Returns:
14 | True iff we're running on GAE.
15 | """
16 | server_software = os.environ.get('GAE_ENV', '')
17 | return server_software.startswith('standard')
18 |
19 |
20 | def _get_project_id():
21 | logging.info("-------------------Running Localy--------------------")
22 | with open('config.json', 'r') as config_file:
23 | config = json.load(config_file)
24 | return config['project']
25 |
26 |
27 | def get_project_id():
28 | """
29 | Return the real or local project id.
30 |
31 | :return: project_id
32 | """
33 | if detect_gae():
34 | project = os.environ.get('GOOGLE_CLOUD_PROJECT',
35 | 'Specified environment variable is not set.')
36 | else:
37 | project = _get_project_id()
38 | return project
39 |
40 |
41 | def get_host_name():
42 | """
43 | Return the real or local hostname.
44 |
45 | :return: hostname
46 | """
47 | if detect_gae():
48 | hostname = '{}.appspot.com'.format(get_project_id())
49 | else:
50 | hostname = '{}.appspot.com'.format(_get_project_id())
51 | return hostname
52 |
53 |
54 | def fatal_code(e):
55 | """
56 | In case of a 500+ errcode do backoff.
57 |
58 | :param e: execption
59 | :return:
60 | """
61 | return e.resp.status < 500
62 |
63 |
64 | def get_next_idx(idx, matrix_size):
65 | """
66 |
67 | Args: Get the next index in the matrix.
68 | idx: current index
69 | matrix_size: matrix size
70 |
71 | Returns:
72 |
73 | """
74 | if idx + 1 == matrix_size:
75 | return 0
76 | else:
77 | return idx + 1
78 |
79 |
80 | def get_prev_idx(idx, matrix_size):
81 | """
82 | Get the previous index in the matrix.
83 | Args:
84 | idx: current index
85 | matrix_size: matrix size
86 |
87 | Returns:
88 |
89 | """
90 | if idx == 0:
91 | return matrix_size - 1
92 | else:
93 | return idx - 1
94 |
--------------------------------------------------------------------------------
/client/src/modules/api/schedule.js:
--------------------------------------------------------------------------------
1 | class ScheduleService {
2 | list = async () => {
3 | const response = await fetch(`/api/v1/list_schedules?verbose=true`, {
4 | method: 'GET',
5 | credentials: 'same-origin',
6 | });
7 |
8 | if (!response.ok) {
9 | console.error(response);
10 | const responseBody = await response.text();
11 | throw Error(responseBody || response.statusText);
12 | }
13 |
14 | return response.json();
15 | };
16 |
17 | get = async (schedule) => {
18 | const response = await fetch(`/api/v1/get_schedule?schedule=${schedule}`, {
19 | method: 'GET',
20 | credentials: 'same-origin',
21 | });
22 |
23 | if (!response.ok) {
24 | console.error(response);
25 | const responseBody = await response.text();
26 | throw Error(responseBody || response.statusText);
27 | }
28 |
29 | return response.json();
30 | };
31 |
32 | delete = async (schedule) => {
33 | const response = await fetch(`/api/v1/del_schedule?schedule=${schedule}`, {
34 | method: 'GET',
35 | credentials: 'same-origin',
36 | });
37 |
38 | if (!response.ok) {
39 | const responseBody = await response.text();
40 | throw Error(responseBody || response.statusText);
41 | }
42 |
43 | return response;
44 | };
45 |
46 | add = async (schedule) => {
47 | const response = await fetch(`/api/v1/add_schedule`, {
48 | method: 'POST',
49 | credentials: 'same-origin',
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | },
53 | body: JSON.stringify(schedule),
54 | });
55 |
56 | if (!response.ok) {
57 | console.error(response);
58 | const responseBody = await response.text();
59 | throw Error(responseBody || response.statusText);
60 | }
61 |
62 | return response;
63 | };
64 |
65 | timezones = async () => {
66 | const response = await fetch(`/api/v1/time_zones`, {
67 | method: 'GET',
68 | credentials: 'same-origin',
69 | });
70 |
71 | if (!response.ok) {
72 | console.error(response);
73 | const responseBody = await response.text();
74 | throw Error(responseBody || response.statusText);
75 | }
76 |
77 | return response.json();
78 | };
79 | }
80 |
81 | export default ScheduleService;
82 |
--------------------------------------------------------------------------------
/client/src/pages/AppIndex.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Recompose
5 | import { compose } from 'react-recompose';
6 |
7 | // Router
8 | import { Switch, Route, Redirect } from 'react-router-dom';
9 |
10 | // Material-UI
11 | import { withStyles } from '@material-ui/core/styles';
12 |
13 | // Project
14 | import withRoot from '../withRoot';
15 | import withProps from '../withProps';
16 | import AppFrame from '../modules/components/AppFrame';
17 |
18 | // Project Views
19 | import NotFound from './NotFound/NotFound';
20 |
21 | import ScheduleList from './Schedule/ScheduleList';
22 | import ScheduleCreate from './Schedule/ScheduleCreate';
23 | import ScheduleEdit from './Schedule/ScheduleEdit';
24 |
25 | import Policy from './Policy/Policy';
26 | import PolicyList from './Policy/PolicyList';
27 |
28 | const styles = (theme) => ({
29 | '@global': {
30 | 'html, body, #root': {
31 | height: '100%',
32 | },
33 | },
34 | root: {},
35 | });
36 |
37 | class Index extends React.Component {
38 | render() {
39 | const { classes } = this.props;
40 |
41 | return (
42 |
43 |
44 | }
48 | />
49 |
50 |
51 |
52 |
57 |
58 |
63 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
77 | Index.propTypes = {
78 | classes: PropTypes.object.isRequired,
79 | };
80 |
81 | export default compose(withRoot, withStyles(styles))(Index);
82 |
--------------------------------------------------------------------------------
/util/gcp.py:
--------------------------------------------------------------------------------
1 | """"GCP utils"""
2 |
3 | import logging
4 |
5 | import backoff
6 | import googleapiclient.discovery
7 | from googleapiclient.errors import HttpError
8 | from util import utils
9 |
10 |
11 | def get_regions():
12 | """
13 | Get all available regions.
14 |
15 | :return: all regions
16 | """
17 | compute = googleapiclient.discovery.build("compute", "v1", cache_discovery=False)
18 |
19 | request = compute.regions().list(project=utils.get_project_id())
20 |
21 | response = request.execute()
22 | rg = []
23 | for region in response["items"]:
24 | rg.append(region["description"])
25 | return rg
26 |
27 |
28 | def get_zones():
29 | """
30 | Get all available zones.
31 |
32 | :return: all regions
33 | """
34 | compute = googleapiclient.discovery.build("compute", "v1", cache_discovery=False)
35 |
36 | request = compute.zones().list(project=utils.get_project_id())
37 |
38 | response = request.execute()
39 | zones = []
40 | for region in response["items"]:
41 | zones.append(region["description"])
42 | return zones
43 |
44 |
45 | def get_instancegroup_no_of_nodes_from_url(url):
46 | """
47 | Get no of instances in a group.
48 |
49 | :return: number
50 | """
51 | compute = googleapiclient.discovery.build("compute", "v1", cache_discovery=False)
52 | url = url[47:]
53 | project = url[: url.find("/")]
54 | zone = url[url.find("zones") + 6 : url.find("instanceGroupManagers") - 1]
55 | pool = url[url.rfind("/") + 1 :]
56 | res = (
57 | compute.instanceGroups()
58 | .get(project=project, zone=zone, instanceGroup=pool)
59 | .execute()
60 | )
61 | return res["size"]
62 |
63 |
64 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
65 | def resize_node_pool(size, url):
66 | """
67 | resize a node pool
68 | Args:
69 | size: requested size
70 | url: instance group url
71 |
72 | Returns:
73 |
74 | """
75 | compute = googleapiclient.discovery.build("compute", "v1", cache_discovery=False)
76 | url = url[47:]
77 | project = url[: url.find("/")]
78 | zone = url[url.find("zones") + 6 : url.find("instanceGroupManagers") - 1]
79 | instance_group_manager = url[url.rfind("/") + 1 :]
80 | try:
81 | res = (
82 | compute.instanceGroupManagers()
83 | .resize(
84 | project=project,
85 | zone=zone,
86 | instanceGroupManager=instance_group_manager,
87 | size=size,
88 | )
89 | .execute()
90 | )
91 | except Exception as e:
92 | logging.error(e)
93 | return res
94 |
--------------------------------------------------------------------------------
/tasks/policy_tasks.py:
--------------------------------------------------------------------------------
1 | """Check if there is a need to take an action for a policy."""
2 | import json
3 | import logging
4 |
5 | import numpy as np
6 | from google.cloud import ndb, tasks_v2
7 | from model.policymodel import PolicyModel
8 | from model.schedulesmodel import SchedulesModel
9 | from util import location, tz, utils
10 |
11 |
12 | def policy_checker(name):
13 | """
14 | Check if there is a need to take an action for a policy.
15 | Args:
16 | name: policy name
17 |
18 | Returns:
19 |
20 | """
21 |
22 | policy = PolicyModel.query(PolicyModel.Name == name).get()
23 | if not policy:
24 | logging.error("Policy %s not found!", name)
25 | return "not found", 404
26 | schedule = SchedulesModel.query(SchedulesModel.Name == policy.Schedule).get()
27 | if not schedule:
28 | logging.error("Schedule %s not found!", policy.Schedule)
29 | return "not found", 404
30 | logging.debug(
31 | "Time at Timezone %s is %s",
32 | schedule.Timezone,
33 | tz.get_time_at_timezone(schedule.Timezone),
34 | )
35 | day, hour = tz.convert_time_to_index(tz.get_time_at_timezone(schedule.Timezone))
36 | logging.debug("Working on day %s hour %s", day, hour)
37 | arr = np.asarray(schedule.Schedule["__ndarray__"], dtype=np.int).flatten()
38 | matrix_size = schedule.Schedule["Shape"][0] * schedule.Schedule["Shape"][1]
39 | prev = utils.get_prev_idx(day * 24 + hour, matrix_size)
40 | now = arr[day * 24 + hour]
41 | prev = arr[prev]
42 | logging.info("Previous state %s current %s", prev, now)
43 | if now == prev:
44 | # do nothing
45 | logging.info("Conditions are met, Nothing should be done for %s", name)
46 | return "ok", 200
47 | else:
48 | # stop/start
49 | logging.info("State is changing for %s to %s", name, now)
50 | # for each tag lets do it
51 |
52 | task_client = tasks_v2.CloudTasksClient()
53 | task = {
54 | "app_engine_http_request": { # Specify the type of request.
55 | "http_method": "POST",
56 | "relative_uri": "/tasks/change_state",
57 | }
58 | }
59 | parent = task_client.queue_path(
60 | queue="zorya-tasks",
61 | project=utils.get_project_id(),
62 | location=location.get_location(),
63 | )
64 | for tag in policy.Tags:
65 | for project in policy.Projects:
66 | payload = {
67 | "project": project,
68 | "tagkey": next(iter(tag)),
69 | "tagvalue": tag[next(iter(tag))],
70 | "action": str(now),
71 | }
72 | task["app_engine_http_request"]["body"] = (
73 | json.dumps(payload)
74 | ).encode()
75 | response = task_client.create_task(parent, task)
76 | logging.debug("Task %s enqueued", response.name)
77 | return "ok", 200
78 |
--------------------------------------------------------------------------------
/gcp/gae.py:
--------------------------------------------------------------------------------
1 | """Interactions with compute engine."""
2 | import logging
3 |
4 | import backoff
5 | from googleapiclient import discovery
6 | from googleapiclient.errors import HttpError
7 | from util import utils
8 |
9 | CREDENTIALS = None
10 |
11 |
12 | class Gae(object):
13 | """App Engine actions."""
14 |
15 | def __init__(self, project):
16 | self.app = discovery.build(
17 | 'appengine', 'v1', credentials=CREDENTIALS, cache_discovery=False
18 | )
19 | self.project = project
20 |
21 | def change_status(self, to_status, service_id, version_id):
22 | """
23 | Stop/start version based on tags
24 | Args:
25 | to_status: 0 stop 1 start
26 | service_id: The App Engine service id
27 | version_id: The App Engine version id
28 |
29 | Returns:
30 |
31 | """
32 |
33 | try:
34 | if int(to_status) == 1:
35 | logging.info(
36 | "Starting App Engine service: %s version: %s on project: %s",
37 | service_id,
38 | version_id,
39 | self.project
40 | )
41 | self.start_version(service_id, version_id)
42 |
43 | else:
44 | logging.info(
45 | "Stopping App Engine service: %s version: %s on project: %s",
46 | service_id,
47 | version_id,
48 | self.project
49 | )
50 | self.stop_version(service_id, version_id)
51 |
52 | except HttpError as http_error:
53 | logging.error(http_error)
54 | return "Error", 500
55 | return "ok", 200
56 |
57 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
58 | def stop_version(self, service_id, version_id):
59 | """
60 | Stop an instance.
61 | Args:
62 | service_id: The App Engine service id
63 | version_id: The App Engine version id
64 |
65 | Returns:
66 |
67 | """
68 | # TODO add requestId
69 | return (
70 | self.app.apps().services().versions().patch(servicesId=service_id, appsId=self.project,
71 | versionsId=version_id, updateMask='servingStatus',
72 | body={"servingStatus": "STOPPED"}).execute()
73 | )
74 |
75 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
76 | def start_version(self, service_id, version_id):
77 | """
78 | Start an instance.
79 | Args:
80 | service_id: The App Engine service id
81 | version_id: The App Engine version id
82 |
83 | Returns:
84 |
85 | """
86 | # TODO add requestId
87 | return (
88 | self.app.apps().services().versions().patch(servicesId=service_id, appsId=self.project,
89 | versionsId=version_id, updateMask='servingStatus',
90 | body={"servingStatus": "SERVING"}).execute()
91 | )
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zorya
2 |
3 | Schedule GCE Instances, Cloud SQL and GKE node pools
4 |
5 | [Blog Post](http://bit.ly/zorya_blog)
6 |
7 | [](LICENSE) [](https://github.com/doitintl/zorya)
8 |
9 | In Slavic mythology, [Zoryas](https://www.wikiwand.com/en/Zorya) are two guardian goddesses. The Zoryas represent the morning star and the evening star, — if you have read or watched Neil Gaiman’s American Gods, you will probably remember these sisters).
10 |
11 | ## Installation
12 |
13 | ```shell
14 | pip install -r requirements.txt -t lib
15 | ```
16 |
17 | Next step: Download and install [Yarn](https://yarnpkg.com/).
18 |
19 | ### Known Issues for Installation
20 |
21 | * Deployment from Google Cloud Shell fails with an error [#25](https://github.com/doitintl/zorya/issues/25).
22 |
23 | * Building on macOS running on Apple Silicon (arm M1) may fail due to issues building grpcio. Use the following workaround:
24 |
25 | >```shell
26 | >GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1 \
27 | > GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1 \
28 | > pip install -r requirements.txt -t lib
29 | >```
30 |
31 | ## Enable required GCP APIs:
32 |
33 | * Cloud Tasks
34 | * App Engine
35 | * Cloud Storage
36 | * Datastore
37 | * IAP
38 | * Cloud Build
39 | * Cloud Scheduler
40 | * Compute Engine
41 | * Cloud SQL Admin API
42 |
43 | ## Deploy Backend and GUI:
44 | ```shell
45 | ./deploy.sh project-id
46 | ```
47 |
48 |
49 | #### Access the app
50 | ```shell
51 | gcloud app browse
52 | ```
53 |
54 | **WARNING**: By default this application is public; ensure you turn on IAP, as follows:
55 |
56 | To sign into the app, we are using [Cloud Identity-Aware Proxy (Cloud IAP)](https://cloud.google.com/iap/). Cloud IAP works by verifying a user’s identity and determining if that user should be allowed to access the application. The setup is as simple as heading over to [GCP console](https://console.cloud.google.com/iam-admin/iap), enabling IAP on your GAE app and adding the users who should have access to it.
57 |
58 | #### Authorization
59 |
60 | For Zorya to work, its service account requires the folling roles:
61 |
62 | * Cloud Tasks Enqueuer
63 | * Cloud Datastore User
64 | * Logs Writer
65 |
66 | For any project that Zorya is supposed to be managing resources for, Zorya's service account requires the following additional roles:
67 |
68 | * Compute Instance Admin (v1)
69 | * Kubernetes Engine Cluster Admin
70 | * Cloud SQL Editor
71 |
72 | 
73 |
74 | The name of the service account you will need to assign permissions to is as following:`@appspot.gserviceaccount.com` and will have been automatically created by Google App Engine. *NOTE:* this is done under *IAM*, selecting the account, choosing *Permissions* and then adding the roles above to it; not under *Service Accounts*.
75 |
76 | ## Flow
77 |
78 | * Every hour on the hour a cron job calls `/tasks/schedule` which loop over all the policies
79 | * We are checking the desired state vs the previous hour desired state of Zorya states. If they are not the same we will apply the change.
80 |
81 | [API Documentation](http://bit.ly/zorya_api_docs)
82 |
83 | ### Creating a Schedule
84 |
85 | 
86 |
87 | ### Creating a Policy
88 |
89 | 
90 |
91 | App Engine Flex is now supported - in order to add an App engine flex to the policy please look at [Adding GAE Flex to Zorya policy](./App%20Engine%20Flex%20Tagging.md).
92 |
--------------------------------------------------------------------------------
/client/src/modules/components/ScheduleTimeZone.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import TextField from '@material-ui/core/TextField';
5 | import Paper from '@material-ui/core/Paper';
6 | import MenuItem from '@material-ui/core/MenuItem';
7 | import Downshift from 'downshift';
8 |
9 | function renderInput(inputProps) {
10 | const { InputProps, classes, ref, ...other } = inputProps;
11 |
12 | return (
13 |
24 | );
25 | }
26 |
27 | function renderSuggestion(params) {
28 | const { suggestion, index, itemProps, highlightedIndex, selectedItem } =
29 | params;
30 | const isHighlighted = highlightedIndex === index;
31 | const isSelected = selectedItem === suggestion;
32 |
33 | return (
34 |
45 | );
46 | }
47 |
48 | const styles = (theme) => ({
49 | container: {
50 | width: 250,
51 | marginBottom: theme.spacing(3),
52 | },
53 | suggestionsContainer: {
54 | position: 'absolute',
55 | maxHeight: 200,
56 | width: 250,
57 | overflow: 'auto',
58 | zIndex: 1000,
59 | },
60 | });
61 |
62 | class ScheduleTimezone extends React.Component {
63 | constructor(props, context) {
64 | super(props, context);
65 | this.state = {};
66 | }
67 |
68 | getSuggestions = (inputValue) =>
69 | this.props.timezones
70 | .filter(
71 | (suggestion) =>
72 | !inputValue ||
73 | suggestion.toLowerCase().includes(inputValue.toLowerCase())
74 | )
75 | .slice(0, 20);
76 |
77 | render() {
78 | const { classes, onSelect, selected } = this.props;
79 |
80 | return (
81 |
82 | {({
83 | getInputProps,
84 | getItemProps,
85 | isOpen,
86 | inputValue,
87 | selectedItem,
88 | highlightedIndex,
89 | }) => (
90 |
91 | {renderInput({
92 | fullWidth: true,
93 | classes,
94 | InputProps: getInputProps({
95 | placeholder: 'Search timezone',
96 | id: 'timezone',
97 | }),
98 | })}
99 | {isOpen ? (
100 |
105 | {this.getSuggestions(inputValue).map((suggestion, index) =>
106 | renderSuggestion({
107 | suggestion,
108 | index,
109 | itemProps: getItemProps({ item: suggestion }),
110 | highlightedIndex,
111 | selectedItem,
112 | })
113 | )}
114 |
115 | ) : null}
116 |
117 | )}
118 |
119 | );
120 | }
121 | }
122 |
123 | ScheduleTimezone.propTypes = {
124 | classes: PropTypes.object.isRequired,
125 | };
126 |
127 | export default withStyles(styles)(ScheduleTimezone);
128 |
--------------------------------------------------------------------------------
/gcp/compute.py:
--------------------------------------------------------------------------------
1 | """Interactions with compute engine."""
2 | import logging
3 |
4 | import backoff
5 | from googleapiclient import discovery
6 | from googleapiclient.errors import HttpError
7 | from util import gcp, utils
8 |
9 | CREDENTIALS = None
10 |
11 |
12 | class Compute(object):
13 | """Compute engine actions."""
14 |
15 | def __init__(self, project):
16 | self.compute = discovery.build(
17 | "compute", "v1", credentials=CREDENTIALS, cache_discovery=False
18 | )
19 | self.project = project
20 |
21 | def change_status(self, to_status, tagkey, tagvalue):
22 | """
23 | Stop/start instance based on tags
24 | Args:
25 | to_status: 0 stop 1 start
26 | tagkey: tag key
27 | tagvalue: tag value
28 |
29 | Returns:
30 |
31 | """
32 | tag_filter = "labels." + tagkey + "=" + tagvalue
33 | logging.debug("Filter %s", filter)
34 | for zone in gcp.get_zones():
35 | try:
36 | instances = self.list_instances(zone, tag_filter)
37 | for instance in instances:
38 | if int(to_status) == 1:
39 | logging.info(
40 | "Starting %s in project %s tagkey %s tagvalue %s",
41 | instance["name"],
42 | self.project,
43 | tagkey,
44 | tagvalue,
45 | )
46 | self.start_instance(zone, instance["name"])
47 | else:
48 | logging.info(
49 | "Stopping %s in project %s tagkey %s tagvalue %s",
50 | instance["name"],
51 | self.project,
52 | tagkey,
53 | tagvalue,
54 | )
55 | self.stop_instance(zone, instance["name"])
56 | except HttpError as http_error:
57 | logging.error(http_error)
58 | return "Error", 500
59 | return "ok", 200
60 |
61 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
62 | def stop_instance(self, zone, instance):
63 | """
64 | Stop an instance.
65 | Args:
66 | zone: zone
67 | instance: instance name
68 |
69 | Returns:
70 |
71 | """
72 | # TODO add requestId
73 | return (
74 | self.compute.instances()
75 | .stop(project=self.project, zone=zone, instance=instance)
76 | .execute()
77 | )
78 |
79 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
80 | def start_instance(self, zone, instance):
81 | """
82 | Start an instance.
83 | Args:
84 | zone: zone
85 | instance: instance name
86 |
87 | Returns:
88 |
89 | """
90 | # TODO add requestId
91 | return (
92 | self.compute.instances()
93 | .start(project=self.project, zone=zone, instance=instance)
94 | .execute()
95 | )
96 |
97 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
98 | def list_instances(self, zone, tags_filter=None):
99 | """
100 | List all instances in zone with the requested tags
101 | Args:
102 | zone: zone
103 | tags_filter: tags
104 |
105 | Returns:
106 |
107 | """
108 | result = (
109 | self.compute.instances()
110 | .list(project=self.project, zone=zone, filter=tags_filter)
111 | .execute()
112 | )
113 | if "items" in result:
114 | return result["items"]
115 | else:
116 | return []
117 |
--------------------------------------------------------------------------------
/docs/api_curl.txt:
--------------------------------------------------------------------------------
1 |
2 | API docs are also available at bit.ly/zorya_api_docs
3 |
4 | curl --request POST \
5 | --url http://localhost:8080/api/v1/add_schedule \
6 | --header 'Content-Type: application/json' \
7 | --data '{
8 | "Shape": [
9 | 7,
10 | 24
11 | ],
12 | "timezone": "Europe/Amsterdam",
13 | "Corder": true,
14 | "__ndarray__": [
15 | [
16 | 0,
17 | 1,
18 | 1,
19 | 1,
20 | 1,
21 | 1,
22 | 0,
23 | 0,
24 | 0,
25 | 0,
26 | 0,
27 | 0,
28 | 0,
29 | 0,
30 | 0,
31 | 0,
32 | 0,
33 | 0,
34 | 0,
35 | 0,
36 | 0,
37 | 0,
38 | 0,
39 | 0
40 | ],
41 | [
42 | 0,
43 | 0,
44 | 0,
45 | 0,
46 | 0,
47 | 0,
48 | 0,
49 | 0,
50 | 1,
51 | 1,
52 | 1,
53 | 1,
54 | 1,
55 | 1,
56 | 1,
57 | 1,
58 | 1,
59 | 1,
60 | 0,
61 | 0,
62 | 0,
63 | 0,
64 | 0,
65 | 0
66 | ],
67 | [
68 | 0,
69 | 0,
70 | 0,
71 | 0,
72 | 0,
73 | 0,
74 | 0,
75 | 0,
76 | 0,
77 | 0,
78 | 0,
79 | 1,
80 | 1,
81 | 0,
82 | 0,
83 | 0,
84 | 0,
85 | 0,
86 | 0,
87 | 0,
88 | 0,
89 | 0,
90 | 0,
91 | 0
92 | ],
93 | [
94 | 0,
95 | 0,
96 | 0,
97 | 0,
98 | 0,
99 | 0,
100 | 0,
101 | 0,
102 | 0,
103 | 0,
104 | 0,
105 | 0,
106 | 0,
107 | 0,
108 | 0,
109 | 0,
110 | 0,
111 | 0,
112 | 0,
113 | 0,
114 | 0,
115 | 0,
116 | 0,
117 | 0
118 | ],
119 | [
120 | 0,
121 | 0,
122 | 0,
123 | 0,
124 | 0,
125 | 0,
126 | 0,
127 | 0,
128 | 0,
129 | 0,
130 | 0,
131 | 0,
132 | 0,
133 | 0,
134 | 1,
135 | 1,
136 | 1,
137 | 1,
138 | 1,
139 | 0,
140 | 0,
141 | 0,
142 | 0,
143 | 0
144 | ],
145 | [
146 | 0,
147 | 0,
148 | 0,
149 | 0,
150 | 0,
151 | 0,
152 | 0,
153 | 0,
154 | 0,
155 | 0,
156 | 0,
157 | 0,
158 | 0,
159 | 0,
160 | 0,
161 | 0,
162 | 0,
163 | 0,
164 | 0,
165 | 0,
166 | 0,
167 | 0,
168 | 0,
169 | 0
170 | ],
171 | [
172 | 0,
173 | 0,
174 | 0,
175 | 0,
176 | 0,
177 | 0,
178 | 0,
179 | 0,
180 | 0,
181 | 0,
182 | 0,
183 | 0,
184 | 0,
185 | 0,
186 | 0,
187 | 0,
188 | 0,
189 | 0,
190 | 0,
191 | 0,
192 | 0,
193 | 0,
194 | 0,
195 | 0
196 | ]
197 | ],
198 | "dtype": "int64",
199 | "name": "my schedule name"
200 | }'
201 | curl --request GET \
202 | --url http://localhost:8080/api/v1/list_schedules
203 |
204 | curl --request GET \
205 | --url 'http://localhost:8080/api/v1/del_schedule?schedule=my%20schedule%20name'
206 |
207 | curl --request GET \
208 | --url 'http://localhost:8080/api/v1/del_policy?policy=My%20policy' \
209 | --header 'Content-Type: application/json'
210 |
211 | curl --request POST \
212 | --url http://localhost:8080/api/v1/add_policy \
213 | --header 'Content-Type: application/json' \
214 | --data '{
215 | "name": "My policy",
216 | "tags": [
217 | {
218 | "dev": "sleeper",
219 | "staging": "resting"
220 | }
221 | ],
222 | "projetcs": [
223 | "project-x",
224 | "y-project"
225 | ],
226 | "schedulename": "my schedule name"
227 | }'
228 |
229 | curl --request GET \
230 | --url 'http://localhost:8080/api/v1/get_policy?policy=My%20policy' \
231 | --header 'Content-Type: application/json'
232 |
233 | curl --request GET \
234 | --url http://localhost:8080/api/v1/list_policies \
235 | --header 'Content-Type: application/json'
236 |
237 | curl --request GET \
238 | --url 'http://localhost:8080/api/v1/del_policy?policy=My%20policy' \
239 | --header 'Content-Type: application/json'
--------------------------------------------------------------------------------
/gcp/gke.py:
--------------------------------------------------------------------------------
1 | """Interactions with GKE."""
2 |
3 | import logging
4 |
5 | import backoff
6 | from google.cloud import ndb
7 | from googleapiclient import discovery
8 | from googleapiclient.errors import HttpError
9 | from model.gkenoodespoolsmodel import GkeNodePoolModel
10 | from util import gcp, utils
11 |
12 | CREDENTIALS = None
13 |
14 |
15 | class Gke(object):
16 | """GKE engine actions."""
17 |
18 | def __init__(self, project):
19 | self.gke = discovery.build("container", "v1", cache_discovery=False)
20 | self.project = project
21 |
22 | def change_status(self, to_status, tagkey, tagvalue):
23 | logging.debug("GKE change_status")
24 | client = ndb.Client()
25 | with client.context():
26 | try:
27 | clusters = self.list_clusters()
28 | for cluster in clusters:
29 | if (
30 | "resourceLabels" in cluster
31 | and tagkey in cluster["resourceLabels"]
32 | and cluster["resourceLabels"][tagkey] == tagvalue
33 | ):
34 | logging.debug("GKE change_status cluster %s %s %s", cluster,cluster["resourceLabels"],cluster["resourceLabels"][tagkey])
35 | for nodePool in cluster["nodePools"]:
36 | logging.debug(nodePool["instanceGroupUrls"])
37 | for instanceGroup in nodePool["instanceGroupUrls"]:
38 | url = instanceGroup
39 | node_pool_name = url[url.rfind("/") + 1 :]
40 | no_of_nodes = gcp.get_instancegroup_no_of_nodes_from_url(
41 | url
42 | )
43 | if int(to_status) == 1:
44 | logging.debug(
45 | "Sizing up node pool %s in cluster %s "
46 | "tagkey "
47 | "%s tagvalue %s",
48 | nodePool["name"],
49 | cluster["name"],
50 | tagkey,
51 | tagvalue,
52 | )
53 | res = GkeNodePoolModel.query(
54 | GkeNodePoolModel.Name == node_pool_name
55 | ).get()
56 | logging.debug(res)
57 | if not res:
58 | continue
59 | gcp.resize_node_pool(res.NumberOfNodes, url)
60 | res.key.delete()
61 | else:
62 | logging.debug(
63 | "Sizing down node pool %s in cluster %s "
64 | "tagkey "
65 | "%s tagvalue %s",
66 | nodePool["name"],
67 | cluster["name"],
68 | tagkey,
69 | tagvalue,
70 | )
71 | if no_of_nodes == 0:
72 | continue
73 | node_pool_model = GkeNodePoolModel()
74 | node_pool_model.Name = node_pool_name
75 | node_pool_model.NumberOfNodes = no_of_nodes
76 | node_pool_model.key = ndb.Key(
77 | "GkeNodePoolModel", node_pool_name
78 | )
79 | node_pool_model.put()
80 | gcp.resize_node_pool(0, url)
81 | except HttpError as http_error:
82 | logging.error(http_error)
83 | return "Error", 500
84 | return "ok", 200
85 |
86 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
87 | def list_clusters(self):
88 | """
89 | List all clusters with the requested tags
90 | Args:
91 | zone: zone
92 | tags_filter: tags
93 |
94 | Returns:
95 |
96 | """
97 | parent = "projects/%s/locations/-" % self.project
98 | result = (
99 | self.gke.projects().locations().clusters().list(parent=parent).execute()
100 | )
101 | if "clusters" in result:
102 | return result["clusters"]
103 | else:
104 | return []
105 |
--------------------------------------------------------------------------------
/client/src/modules/components/PolicyTags.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Material UI
5 | import { withStyles } from '@material-ui/core/styles';
6 | import TextField from '@material-ui/core/TextField';
7 | import FormGroup from '@material-ui/core/FormGroup';
8 | import Button from '@material-ui/core/Button';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import ClearIcon from '@material-ui/icons/Clear';
11 |
12 | // Lodash
13 | import map from 'lodash/map';
14 | import forOwn from 'lodash/forOwn';
15 |
16 | const TEXT_FIELD_WIDTH = 250;
17 |
18 | const styles = (theme) => ({
19 | root: {
20 | marginBottom: theme.spacing(3),
21 | },
22 | textField: {
23 | width: TEXT_FIELD_WIDTH,
24 | marginRight: theme.spacing(1),
25 | marginBottom: theme.spacing(1),
26 | },
27 | iconButton: {
28 | width: 32,
29 | height: 32,
30 | },
31 | addButton: {
32 | width: TEXT_FIELD_WIDTH * 2 + theme.spacing(1),
33 | },
34 | sizeSmallButton: {
35 | padding: 0,
36 | minHeight: 24,
37 | },
38 | });
39 |
40 | class PolicyTags extends React.Component {
41 | constructor(props, context) {
42 | super(props, context);
43 | this.state = {
44 | tags: [
45 | {
46 | key: '',
47 | value: '',
48 | },
49 | ],
50 | };
51 | }
52 |
53 | componentDidMount() {
54 | if (this.props.tags && this.props.tags.length > 0) {
55 | let tags = [];
56 | this.props.tags.forEach((tag) => {
57 | forOwn(tag, (value, key) => {
58 | tags.push({
59 | key,
60 | value,
61 | });
62 | });
63 | });
64 | this.setState({
65 | tags,
66 | });
67 | }
68 | }
69 |
70 | publishChanges = (shouldUpdateErrors) => {
71 | const tags = map(this.state.tags, (tag) => ({
72 | [tag.key]: tag.value,
73 | }));
74 | this.props.onChange(tags, shouldUpdateErrors);
75 | };
76 |
77 | handleChange = (index, name) => (event) => {
78 | const tags = this.state.tags.slice();
79 | tags[index][name] = event.target.value;
80 | this.setState({ tags }, () => this.publishChanges(false));
81 | };
82 |
83 | handleClearTag = (index) => (event) => {
84 | const tags = this.state.tags.slice();
85 | if (tags.length > 1) {
86 | tags.splice(index, 1);
87 | this.setState({ tags }, () => this.publishChanges(true));
88 | }
89 | };
90 |
91 | handleAddTag = (event) => {
92 | const tags = this.state.tags.slice();
93 | tags.push({
94 | key: '',
95 | value: '',
96 | });
97 | this.setState({ tags }, () => this.publishChanges(false));
98 | };
99 |
100 | render() {
101 | const { classes, error } = this.props;
102 | const { tags } = this.state;
103 |
104 | return (
105 |
106 | {map(tags, (tag, index) => (
107 |
108 |
118 |
128 |
129 | {tags.length > 1 && (
130 |
138 |
139 |
140 | )}
141 |
142 | ))}
143 |
144 | {tags.length < 7 && (
145 |
157 | )}
158 |
159 | );
160 | }
161 | }
162 | PolicyTags.propTypes = {
163 | classes: PropTypes.object.isRequired,
164 | onChange: PropTypes.func.isRequired,
165 | error: PropTypes.array.isRequired,
166 | };
167 |
168 | export default withStyles(styles)(PolicyTags);
169 |
--------------------------------------------------------------------------------
/gcp/sql.py:
--------------------------------------------------------------------------------
1 | """Interactions with compute engine."""
2 |
3 | import logging
4 |
5 | import backoff
6 | from googleapiclient import discovery
7 | from googleapiclient.errors import HttpError
8 | from util import utils
9 |
10 | CREDENTIALS = None
11 |
12 |
13 | class Sql(object):
14 | """Compute engine actions."""
15 |
16 | def __init__(self, project):
17 | self.sql = discovery.build(
18 | "sqladmin", "v1beta4", credentials=CREDENTIALS, cache_discovery=False
19 | )
20 | self.project = project
21 |
22 | def change_status(self, to_status, tagkey, tagvalue):
23 | """
24 | Stop/start instance based on tags
25 | Args:
26 | to_status: 0 stop 1 start
27 | tagkey: tag key
28 | tagvalue: tag value
29 |
30 | Returns:
31 |
32 | """
33 | tag_filter = "settings.userLabels." + tagkey + "=" + tagvalue
34 | logging.debug("Filter %s", filter)
35 | try:
36 | instances = self.list_instances(tag_filter)
37 | for instance in instances:
38 | if int(to_status) == 1:
39 | logging.info(
40 | "Starting SQL %s in project %s tagkey %s tagvalue %s",
41 | instance["name"],
42 | self.project,
43 | tagkey,
44 | tagvalue,
45 | )
46 | self.start_instance(instance["name"])
47 | else:
48 | logging.info(
49 | "Stopping SQL %s in project %s tagkey %s tagvalue %s",
50 | instance["name"],
51 | self.project,
52 | tagkey,
53 | tagvalue,
54 | )
55 | self.stop_instance(instance["name"])
56 | except HttpError as http_error:
57 | logging.error(http_error)
58 | return "Error", 500
59 | return "ok", 200
60 |
61 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
62 | def stop_instance(self, instance):
63 | """
64 | Stop an instance.
65 | Args:
66 | zone: zone
67 | instance: instance name
68 |
69 | Returns:
70 |
71 | """
72 | # TODO add requestId
73 | try:
74 | prev_instance_data = (
75 | self.sql.instances()
76 | .get(project=self.project, instance=instance)
77 | .execute()
78 | )
79 |
80 | patch_body = {
81 | "settings": {
82 | "settingsVersion": prev_instance_data["settings"]["settingsVersion"],
83 | "activationPolicy": "NEVER"
84 | }
85 | }
86 |
87 | res = (
88 | self.sql.instances()
89 | .patch(
90 | project=self.project,
91 | instance=instance,
92 | body=patch_body
93 | )
94 | .execute()
95 | )
96 | return res
97 | except Exception as e:
98 | logging.error(e)
99 | return
100 |
101 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
102 | def start_instance(self, instance):
103 | """
104 | Start an instance.
105 | Args:
106 | zone: zone
107 | instance: instance name
108 |
109 | Returns:
110 |
111 | """
112 | try:
113 | prev_instance_data = (
114 | self.sql.instances()
115 | .get(project=self.project, instance=instance)
116 | .execute()
117 | )
118 |
119 | patch_body = {
120 | "settings": {
121 | "settingsVersion": prev_instance_data["settings"]["settingsVersion"],
122 | "activationPolicy": "ALWAYS"
123 | }
124 | }
125 |
126 | res = (
127 | self.sql.instances()
128 | .patch(
129 | project=self.project,
130 | instance=instance,
131 | body=patch_body
132 | )
133 | .execute()
134 | )
135 | return res
136 | except Exception as e:
137 | logging.error(e)
138 | return
139 |
140 | @backoff.on_exception(backoff.expo, HttpError, max_tries=8, giveup=utils.fatal_code)
141 | def list_instances(self, tags_filter=None):
142 | """
143 | List all instances in with the requested tags
144 | Args:
145 | zone: zone
146 | tags_filter: tags
147 |
148 | Returns:
149 |
150 | """
151 | result = (
152 | self.sql.instances()
153 | .list(project=self.project, filter=tags_filter)
154 | .execute()
155 | )
156 | if "items" in result:
157 | return result["items"]
158 | else:
159 | return []
160 |
--------------------------------------------------------------------------------
/client/src/modules/components/AppFrame.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | // Recompose
6 | import { compose } from 'react-recompose';
7 |
8 | // Router
9 | import { withRouter } from 'react-router-dom';
10 |
11 | import { withStyles } from '@material-ui/core/styles';
12 | import Drawer from '@material-ui/core/Drawer';
13 | import AppBar from '@material-ui/core/AppBar';
14 | import Toolbar from '@material-ui/core/Toolbar';
15 | import List from '@material-ui/core/List';
16 | import Typography from '@material-ui/core/Typography';
17 | import Divider from '@material-ui/core/Divider';
18 | import ListItem from '@material-ui/core/ListItem';
19 | import ListItemIcon from '@material-ui/core/ListItemIcon';
20 | import ListItemText from '@material-ui/core/ListItemText';
21 | import IconButton from '@material-ui/core/IconButton';
22 | import Hidden from '@material-ui/core/Hidden';
23 |
24 | import PolicyIcon from '@material-ui/icons/LibraryBooks';
25 | import ScheduleIcon from '@material-ui/icons/Schedule';
26 | import MenuIcon from '@material-ui/icons/Menu';
27 |
28 | // Lodash
29 | import map from 'lodash/map';
30 | import find from 'lodash/find';
31 | import startsWith from 'lodash/startsWith';
32 |
33 | // Project
34 | import logo from '../../assets/zorya.png';
35 |
36 | const drawerWidth = 210;
37 |
38 | const links = [
39 | {
40 | primary: 'Schedules',
41 | path: '/schedules/browser',
42 | icon: ,
43 | },
44 | {
45 | primary: 'Policies',
46 | path: '/policies/browser',
47 | icon: ,
48 | },
49 | ];
50 |
51 | const styles = (theme) => ({
52 | root: {
53 | width: '100%',
54 | height: '100%',
55 | zIndex: 1,
56 | overflow: 'scroll',
57 | display: 'flex',
58 | },
59 | appBar: {
60 | marginLeft: drawerWidth,
61 | [theme.breakpoints.up('md')]: {
62 | width: `calc(100% - ${drawerWidth}px)`,
63 | },
64 | },
65 | navIconHide: {
66 | [theme.breakpoints.up('md')]: {
67 | display: 'none',
68 | },
69 | },
70 | drawerHeader: {
71 | display: 'flex',
72 | alignItems: 'center',
73 | ...theme.mixins.toolbar,
74 | },
75 | drawerPaper: {
76 | width: drawerWidth,
77 | [theme.breakpoints.up('md')]: {
78 | position: 'relative',
79 | height: '100%',
80 | },
81 | },
82 | drawerDocked: {
83 | height: '100%',
84 | },
85 | content: {
86 | backgroundColor: theme.palette.background.default,
87 | width: '100%',
88 | height: 'calc(100% - 56px)',
89 | marginTop: 56,
90 | [theme.breakpoints.up('sm')]: {
91 | height: 'calc(100% - 64px)',
92 | marginTop: 64,
93 | },
94 | },
95 | });
96 |
97 | class AppFrame extends React.Component {
98 | constructor(props, context) {
99 | super(props, context);
100 | this.state = {
101 | title: '',
102 | mobileOpen: false,
103 | };
104 | }
105 |
106 | componentDidMount() {
107 | const { history } = this.props;
108 | const currentLink = find(links, (link) =>
109 | startsWith(history.location.pathname, link.path)
110 | );
111 | if (currentLink) {
112 | this.setState({
113 | title: currentLink.primary,
114 | });
115 | }
116 | }
117 |
118 | handleClickLink = (link) => (event) => {
119 | const { history } = this.props;
120 | history.push(link.path);
121 | this.setState({
122 | title: `${link.primary}`,
123 | });
124 | };
125 |
126 | handleDrawerToggle = () => {
127 | this.setState((prevState, props) => ({
128 | mobileOpen: !prevState.mobileOpen,
129 | }));
130 | };
131 |
132 | render() {
133 | const { classes, history, children } = this.props;
134 | const { title, mobileOpen } = this.state;
135 |
136 | const drawer = (
137 |
138 |
139 |

140 |
141 |
142 |
143 | {map(links, (link, index) => (
144 |
155 | {link.icon}
156 |
157 |
158 | ))}
159 |
160 |
161 | );
162 |
163 | return (
164 |
165 |
172 |
173 |
179 |
180 |
181 |
182 | {title}
183 |
184 |
185 |
186 |
187 |
188 |
200 | {drawer}
201 |
202 |
203 |
204 |
205 |
214 | {drawer}
215 |
216 |
217 |
218 |
{children}
219 |
220 | );
221 | }
222 | }
223 |
224 | AppFrame.propTypes = {
225 | classes: PropTypes.object.isRequired,
226 | };
227 |
228 | export default compose(withRouter, withStyles(styles))(AppFrame);
229 |
--------------------------------------------------------------------------------
/client/src/pages/Schedule/ScheduleEdit.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Material UI
4 | import { withStyles } from '@material-ui/core/styles';
5 | import Typography from '@material-ui/core/Typography';
6 | import Button from '@material-ui/core/Button';
7 | import ArrowBackIcon from '@material-ui/icons/ArrowBack';
8 | import IconButton from '@material-ui/core/IconButton';
9 | import TextField from '@material-ui/core/TextField';
10 |
11 | // Project
12 | import ScheduleTimeTable from '../../modules/components/ScheduleTimeTable';
13 | import ScheduleTimeZone from '../../modules/components/ScheduleTimeZone';
14 | import AppPageContent from '../../modules/components/AppPageContent';
15 | import AppPageActions from '../../modules/components/AppPageActions';
16 | import ScheduleService from '../../modules/api/schedule';
17 |
18 | const styles = (theme) => ({
19 | root: {
20 | height: '100%',
21 | },
22 | button: {
23 | marginRight: theme.spacing(2),
24 | },
25 | textField: {
26 | width: 350,
27 | marginBottom: theme.spacing(3),
28 | },
29 | });
30 |
31 | class ScheduleEdit extends React.Component {
32 | constructor(props, context) {
33 | super(props, context);
34 | this.state = {
35 | schedule: null,
36 | isLoading: false,
37 | showBackendError: false,
38 | backendErrorTitle: null,
39 | backendErrorMessage: null,
40 | exitPage: null,
41 | };
42 |
43 | this.scheduleService = new ScheduleService();
44 | }
45 |
46 | async componentDidMount() {
47 | const { match } = this.props;
48 | this.setState({ isLoading: true });
49 | try {
50 | const schedule = await this.scheduleService.get(match.params.schedule);
51 | const timezones = await this.scheduleService.timezones();
52 | this.setState({
53 | schedule,
54 | timezones: timezones.Timezones,
55 | isLoading: false,
56 | });
57 | } catch (error) {
58 | this.handleBackendError(
59 | 'Loading Failed:',
60 | error.message,
61 | '/schedules/browser'
62 | );
63 | }
64 | }
65 |
66 | handleChange = (name) => (event) => {
67 | const { schedule } = this.state;
68 | schedule[name] = event.target.value;
69 | this.setState({ schedule });
70 | };
71 |
72 | handleScheduleChange = (nextSchedule) => {
73 | this.setState({
74 | schedule: nextSchedule,
75 | });
76 | };
77 |
78 | handleChangeTimezone = (value) => {
79 | const { schedule } = this.state;
80 | schedule.timezone = value;
81 | this.setState({ schedule });
82 | };
83 |
84 | handleSave = async (event) => {
85 | try {
86 | const { history } = this.props;
87 | const { schedule } = this.state;
88 | this.setState({ isLoading: true });
89 | await this.scheduleService.add(schedule);
90 | this.setState({ isLoading: false });
91 | history.push('/schedules/browser');
92 | } catch (error) {
93 | this.handleBackendError('Saving failed:', error.message);
94 | }
95 | };
96 |
97 | handleRequestCancel = (event) => {
98 | const { history } = this.props;
99 | history.goBack();
100 | };
101 |
102 | handleBackendError = (title, message, exitPage) => {
103 | this.setState({
104 | backendErrorTitle: title,
105 | backendErrorMessage: message,
106 | showBackendError: true,
107 | isLoading: false,
108 | exitPage,
109 | });
110 | };
111 |
112 | handleErrorClose = () => {
113 | const { history } = this.props;
114 | const { exitPage } = this.state;
115 | this.setState({
116 | showBackendError: false,
117 | isLoading: false,
118 | });
119 | if (exitPage) {
120 | history.push(exitPage);
121 | }
122 | };
123 |
124 | render() {
125 | const { classes } = this.props;
126 | const {
127 | schedule,
128 | timezones,
129 | isLoading,
130 | showBackendError,
131 | backendErrorTitle,
132 | backendErrorMessage,
133 | } = this.state;
134 |
135 | return (
136 |
137 |
138 |
143 |
144 |
145 |
146 | Edit schedule {schedule ? schedule.name : ''}
147 |
148 |
149 |
156 | {schedule && (
157 |
158 |
166 |
167 |
175 |
176 |
181 |
182 |
186 |
187 |
196 |
205 |
206 | )}
207 |
208 |
209 | );
210 | }
211 | }
212 |
213 | export default withStyles(styles)(ScheduleEdit);
214 |
--------------------------------------------------------------------------------
/client/src/pages/Schedule/ScheduleCreate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Material UI
4 | import { withStyles } from '@material-ui/core/styles';
5 | import Typography from '@material-ui/core/Typography';
6 | import Button from '@material-ui/core/Button';
7 | import ArrowBackIcon from '@material-ui/icons/ArrowBack';
8 | import IconButton from '@material-ui/core/IconButton';
9 | import TextField from '@material-ui/core/TextField';
10 |
11 | // Project
12 | import ScheduleTimeTable from '../../modules/components/ScheduleTimeTable';
13 | import ScheduleTimeZone from '../../modules/components/ScheduleTimeZone';
14 | import AppPageContent from '../../modules/components/AppPageContent';
15 | import AppPageActions from '../../modules/components/AppPageActions';
16 | import ScheduleService from '../../modules/api/schedule';
17 | import { getDefaultSchedule } from '../../modules/utils/schedule';
18 |
19 | const styles = (theme) => ({
20 | root: {
21 | height: '100%',
22 | },
23 | button: {
24 | marginRight: theme.spacing(2),
25 | },
26 | textField: {
27 | minWidth: 250,
28 | marginBottom: theme.spacing(3),
29 | marginRight: theme.spacing(2),
30 | },
31 | });
32 |
33 | class ScheduleCreate extends React.Component {
34 | constructor(props, context) {
35 | super(props, context);
36 | this.state = {
37 | schedule: getDefaultSchedule(),
38 | nameError: false,
39 | timezones: [],
40 | isLoading: false,
41 | showBackendError: false,
42 | backendErrorTitle: null,
43 | backendErrorMessage: null,
44 | exitPage: null,
45 | };
46 |
47 | this.scheduleService = new ScheduleService();
48 | }
49 |
50 | async componentDidMount() {
51 | try {
52 | this.setState({ isLoading: true });
53 | const response = await this.scheduleService.timezones();
54 | this.setState({
55 | timezones: response.Timezones,
56 | isLoading: false,
57 | });
58 | } catch (error) {
59 | this.handleBackendError(
60 | 'Loading timezones failed:',
61 | error.message,
62 | '/schedules/browser'
63 | );
64 | }
65 | }
66 |
67 | handleChange = (name) => (event) => {
68 | const { schedule } = this.state;
69 | schedule[name] = event.target.value;
70 | this.setState({ schedule });
71 | };
72 |
73 | handleChangeTimezone = (value) => {
74 | const { schedule } = this.state;
75 | schedule.timezone = value;
76 | this.setState({ schedule });
77 | };
78 |
79 | handleScheduleChange = (nextSchedule) => {
80 | this.setState({
81 | schedule: nextSchedule,
82 | });
83 | };
84 |
85 | handleCreate = async (event) => {
86 | try {
87 | const { history } = this.props;
88 | const { schedule } = this.state;
89 | const nameRe = /^[a-zA-Z][\w-]*[a-zA-Z0-9]$/;
90 | if (!nameRe.test(schedule.name)) {
91 | this.setState({
92 | nameError: true,
93 | });
94 | return;
95 | }
96 | this.setState({ isLoading: true });
97 | await this.scheduleService.add(schedule);
98 | this.setState({ isLoading: false });
99 | history.push('/schedules/browser');
100 | } catch (error) {
101 | this.handleBackendError('Saving failed:', error.message);
102 | }
103 | };
104 |
105 | handleRequestCancel = (event) => {
106 | const { history } = this.props;
107 | history.goBack();
108 | };
109 |
110 | handleBackendError = (title, message, exitPage) => {
111 | this.setState({
112 | backendErrorTitle: title,
113 | backendErrorMessage: message,
114 | showBackendError: true,
115 | isLoading: false,
116 | exitPage,
117 | });
118 | };
119 |
120 | handleErrorClose = () => {
121 | const { history } = this.props;
122 | const { exitPage } = this.state;
123 | this.setState({
124 | showBackendError: false,
125 | isLoading: false,
126 | });
127 | if (exitPage) {
128 | history.push(exitPage);
129 | }
130 | };
131 |
132 | render() {
133 | const { classes } = this.props;
134 | const {
135 | schedule,
136 | timezones,
137 | nameError,
138 | isLoading,
139 | showBackendError,
140 | backendErrorTitle,
141 | backendErrorMessage,
142 | } = this.state;
143 |
144 | return (
145 |
146 |
147 |
152 |
153 |
154 |
155 | Create a schedule
156 |
157 |
158 |
159 |
166 |
177 |
178 |
187 |
188 |
193 |
194 |
198 |
199 |
208 |
217 |
218 |
219 | );
220 | }
221 | }
222 |
223 | export default withStyles(styles)(ScheduleCreate);
224 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | """Entry point to Zoyra."""
2 | import logging
3 | import json
4 |
5 | from google.cloud import ndb
6 | from flask import Flask, request
7 | from model.policymodel import PolicyModel
8 | from model.schedulesmodel import SchedulesModel
9 | from tasks import policy_tasks, schedule_tasks
10 | from util import tz
11 |
12 | import google.cloud.logging
13 |
14 | log_client = google.cloud.logging.Client()
15 | log_client.setup_logging()
16 |
17 |
18 | API_VERSION = "/api/v1"
19 | app = Flask(__name__)
20 | client = ndb.Client()
21 |
22 |
23 | @app.route("/tasks/change_state", methods=["POST"])
24 | def change_state():
25 | """
26 | Initiate change state.
27 | Returns:
28 |
29 | """
30 | payload = json.loads(request.get_data(as_text=False) or "(empty payload)")
31 | logging.debug(
32 | "Starting change_state action %s project %s tagkey %s tagvalue %s",
33 | payload["action"],
34 | payload["project"],
35 | payload["tagkey"],
36 | payload["tagvalue"],
37 | )
38 | schedule_tasks.change_state(
39 | payload["tagkey"], payload["tagvalue"], payload["action"], payload["project"]
40 | )
41 | return "ok", 200
42 |
43 |
44 | @app.route("/tasks/schedule", methods=["GET"])
45 | def schedule():
46 | """
47 | Checks if it's time to run a schedule.
48 | Returns:
49 |
50 | """
51 | logging.debug("From Cron start /tasks/schedule")
52 |
53 | with client.context():
54 | keys = PolicyModel.query().fetch(keys_only=True)
55 | for key in keys:
56 | logging.debug("Creating deferred task for %s", key.id())
57 | policy_tasks.policy_checker(key.id())
58 | return "ok", 200
59 |
60 |
61 | @app.route(API_VERSION + "/time_zones", methods=["GET"])
62 | def time_zones():
63 | """
64 | Get all time zones.
65 | :return: all time zone in the world wide world.
66 | """
67 | return json.dumps({"Timezones": tz.get_all_timezones()})
68 |
69 |
70 | @app.route(API_VERSION + "/add_schedule", methods=["POST"])
71 | def add_schedule():
72 | """
73 | Add a schedule.
74 | Returns:
75 |
76 | """
77 | with client.context():
78 | schedules_model = SchedulesModel()
79 | schedules_model.Schedule = {
80 | "dtype": request.json["dtype"],
81 | "Corder": request.json["Corder"],
82 | "Shape": request.json["Shape"],
83 | "__ndarray__": request.json["__ndarray__"],
84 | }
85 |
86 | schedules_model.Name = request.json["name"]
87 | schedules_model.DisplayName = request.json.get(
88 | "displayname", request.json.get("name"))
89 | schedules_model.Timezone = request.json["timezone"]
90 | schedules_model.key = ndb.Key("SchedulesModel", request.json["name"])
91 | schedules_model.put()
92 | return "ok", 200
93 |
94 |
95 | @app.route(API_VERSION + "/get_schedule", methods=["GET"])
96 | def get_schedule():
97 | """
98 | Get a schedule.
99 | Returns: schedule json
100 |
101 | """
102 | name = request.args.get("schedule")
103 | schedule = {}
104 | with client.context():
105 | res = SchedulesModel.query(SchedulesModel.Name == name).get()
106 | if not res:
107 | return "not found", 404
108 | schedule.update({"name": res.Name})
109 | schedule.update({"displayname": res.DisplayName or res.Name})
110 | schedule.update(res.Schedule)
111 | schedule.update({"timezone": res.Timezone})
112 | logging.debug(json.dumps(res.Schedule))
113 | return json.dumps(schedule)
114 |
115 |
116 | @app.route(API_VERSION + "/list_schedules", methods=["GET"])
117 | def list_schedules():
118 | """
119 | Get all schedules.
120 | Returns: A list of schedules
121 |
122 | """
123 | schedules_list = []
124 | with client.context():
125 | verbose = request.args.get("verbose") == "true"
126 | if verbose:
127 | schedules = SchedulesModel.query().fetch()
128 | for schedule in schedules:
129 | schedules_list.append(
130 | {"name": schedule.Name, "displayName": schedule.DisplayName}
131 | )
132 | else:
133 | keys = SchedulesModel.query().fetch(keys_only=True)
134 | for key in keys:
135 | schedules_list.append(key.id())
136 | return json.dumps(schedules_list)
137 |
138 |
139 | @app.route(API_VERSION + "/del_schedule", methods=["GET"])
140 | def del_schedule():
141 | """
142 | Delete a schedule.
143 | Returns:
144 |
145 | """
146 | name = request.args.get("schedule")
147 | with client.context():
148 | res = SchedulesModel.query(SchedulesModel.Name == name).get()
149 | if not res:
150 | return "Schedule '{}' not found.".format(name), 404
151 | policy = PolicyModel.query(PolicyModel.Schedule == name).get()
152 | if policy:
153 | return "Forbidden. Schedule '{}' is in use by policy '{}'.".format(name, policy.Name), 403
154 | res.key.delete()
155 | return "ok", 200
156 |
157 |
158 | @app.route(API_VERSION + "/add_policy", methods=["POST"])
159 | def add_policy():
160 | """
161 | Add policy.
162 | Returns:
163 |
164 | """
165 | logging.debug(json.dumps(request.json))
166 | name = request.json["name"]
167 | display_name = request.json.get("displayname", name)
168 | tags = request.json["tags"]
169 | projects = request.json["projects"]
170 | schedule_name = request.json["schedulename"]
171 | with client.context():
172 | res = SchedulesModel.query(SchedulesModel.Name == schedule_name).get()
173 | if not res:
174 | return "Schedule '{}' not found.".format(schedule_name), 404
175 |
176 | policy_model = PolicyModel()
177 | policy_model.Name = name
178 | policy_model.DisplayName = display_name
179 | policy_model.Tags = tags
180 | policy_model.Projects = projects
181 | policy_model.Schedule = schedule_name
182 | policy_model.key = ndb.Key("PolicyModel", name)
183 | policy_model.put()
184 | return "ok", 200
185 |
186 |
187 | @app.route(API_VERSION + "/get_policy", methods=["GET"])
188 | def get_policy():
189 | """
190 | Get policy.
191 | Returns: policy json
192 |
193 | """
194 | policy = {}
195 | name = request.args.get("policy")
196 | with client.context():
197 | res = PolicyModel.query(PolicyModel.Name == name).get()
198 | logging.debug(res)
199 | if not res:
200 | return "Policy '{}' not found.".format(name), 404
201 | policy.update({"name": res.Name})
202 | policy.update({"displayname": res.DisplayName or res.Name})
203 | policy.update({"schedulename": res.Schedule})
204 | policy.update({"tags": res.Tags})
205 | policy.update({"projects": res.Projects})
206 | return json.dumps(policy)
207 |
208 |
209 | @app.route(API_VERSION + "/list_policies", methods=["GET"])
210 | def list_policies():
211 | """
212 | Get all polices.
213 | Returns: List of policies
214 |
215 | """
216 | policies_list = []
217 | with client.context():
218 | verbose = request.args.get("verbose") == "true"
219 | if verbose:
220 | policies = PolicyModel.query().fetch()
221 | for policy in policies:
222 | policies_list.append(
223 | {"name": policy.Name, "displayName": policy.DisplayName}
224 | )
225 | else:
226 | keys = PolicyModel.query().fetch(keys_only=True)
227 | for key in keys:
228 | policies_list.append(key.id())
229 |
230 | return json.dumps(policies_list)
231 |
232 |
233 | @app.route(API_VERSION + "/del_policy", methods=["GET"])
234 | def del_policy():
235 | """
236 | Delete a policy
237 | Returns:
238 |
239 | """
240 | name = request.args.get("policy")
241 | with client.context():
242 | res = PolicyModel.query(PolicyModel.Name == name).get()
243 | if not res:
244 | return "Policy '{}' not found.".format(name), 404
245 | res.key.delete()
246 | return "ok", 200
247 |
248 |
249 | @app.route("/")
250 | def index():
251 | """
252 | Main Page
253 | :return:
254 | """
255 | return "ok", 200
256 |
257 |
258 | if __name__ == "__main__":
259 | logging.getLogger("googleapiclient.discovery_cache").setLevel(logging.INFO)
260 | app.run(debug=False)
261 |
--------------------------------------------------------------------------------
/client/src/pages/Policy/PolicyList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Recompose
4 | import { compose } from 'react-recompose';
5 |
6 | // Router
7 | import { withRouter } from 'react-router-dom';
8 |
9 | // Material UI
10 | import { withStyles } from '@material-ui/core/styles';
11 | import Table from '@material-ui/core/Table';
12 | import TableBody from '@material-ui/core/TableBody';
13 | import TableCell from '@material-ui/core/TableCell';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import TableSortLabel from '@material-ui/core/TableSortLabel';
17 |
18 | import Tooltip from '@material-ui/core/Tooltip';
19 | import Button from '@material-ui/core/Button';
20 | import Checkbox from '@material-ui/core/Checkbox';
21 | import AddIcon from '@material-ui/icons/Add';
22 | import RefreshIcon from '@material-ui/icons/Refresh';
23 | import EditIcon from '@material-ui/icons/Edit';
24 | import DeleteIcon from '@material-ui/icons/Delete';
25 |
26 | // Lodash
27 | import map from 'lodash/map';
28 | import indexOf from 'lodash/indexOf';
29 |
30 | // Project
31 | import PolicyService from '../../modules/api/policy';
32 | import AppPageContent from '../../modules/components/AppPageContent';
33 | import AppPageActions from '../../modules/components/AppPageActions';
34 |
35 | const styles = (theme) => ({
36 | root: {
37 | height: '100%',
38 | },
39 | button: {
40 | marginRight: theme.spacing(2),
41 | },
42 | leftIcon: {
43 | marginRight: theme.spacing(1),
44 | },
45 | link: {
46 | '&:hover': {
47 | textDecoration: 'underline',
48 | cursor: 'pointer',
49 | },
50 | },
51 | checkboxCell: {
52 | width: 48,
53 | },
54 | });
55 |
56 | class PolicyList extends React.Component {
57 | constructor(props, context) {
58 | super(props, context);
59 | this.state = {
60 | policies: [],
61 | selected: [],
62 | order: 'asc',
63 | isLoading: false,
64 | showBackendError: false,
65 | backendErrorTitle: null,
66 | backendErrorMessage: null,
67 | };
68 |
69 | this.policyService = new PolicyService();
70 | }
71 |
72 | componentDidMount() {
73 | this.refreshList();
74 | }
75 |
76 | handleRequestSort = (event) => {
77 | this.setState((prevState, props) => {
78 | let order = 'desc';
79 | if (prevState.order === 'desc') {
80 | order = 'asc';
81 | }
82 |
83 | const policies =
84 | order === 'desc'
85 | ? prevState.policies.sort((a, b) =>
86 | b.displayname || b.name < a.displayname || a.name ? -1 : 1
87 | )
88 | : prevState.policies.sort((a, b) =>
89 | a.displayname || a.name < b.displayname || b.name ? -1 : 1
90 | );
91 |
92 | return {
93 | policies,
94 | order,
95 | };
96 | });
97 | };
98 |
99 | handleClickNavigate = (path) => (event) => {
100 | const { history } = this.props;
101 | history.push(path);
102 | };
103 |
104 | handleClickRefresh = (event) => {
105 | this.refreshList();
106 | };
107 |
108 | refreshList = async () => {
109 | this.setState({ isLoading: true });
110 | try {
111 | const policies = await this.policyService.list();
112 | this.setState({
113 | policies,
114 | isLoading: false,
115 | });
116 | } catch (error) {
117 | this.handleBackendError('Loading Failed:', error.message);
118 | }
119 | };
120 |
121 | handleClick = (event, policy) => {
122 | const { selected } = this.state;
123 | const selectedIndex = indexOf(selected, policy.name);
124 | let newSelected = [];
125 |
126 | if (selectedIndex === -1) {
127 | newSelected = newSelected.concat(selected, policy.name);
128 | } else if (selectedIndex === 0) {
129 | newSelected = newSelected.concat(selected.slice(1));
130 | } else if (selectedIndex === selected.length - 1) {
131 | newSelected = newSelected.concat(selected.slice(0, -1));
132 | } else if (selectedIndex > 0) {
133 | newSelected = newSelected.concat(
134 | selected.slice(0, selectedIndex),
135 | selected.slice(selectedIndex + 1)
136 | );
137 | }
138 |
139 | this.setState({ selected: newSelected });
140 | };
141 |
142 | handleSelectAllClick = (event, checked) => {
143 | if (checked) {
144 | this.setState({ selected: this.state.policies.map((p) => p.name) });
145 | } else {
146 | this.setState({ selected: [] });
147 | }
148 | };
149 |
150 | handleDeleteClick = async (event) => {
151 | try {
152 | const { selected } = this.state;
153 | if (selected.length > 0) {
154 | const promises = [];
155 | this.setState({ isLoading: true });
156 | selected.forEach((policy) => {
157 | promises.push(
158 | this.policyService.delete(policy).catch((error) => error)
159 | );
160 | });
161 | const responses = await Promise.all(promises);
162 | const errorMessages = responses
163 | .filter((response) => response instanceof Error)
164 | .map((error) => error.message);
165 | if (errorMessages.length) {
166 | throw Error(errorMessages.join('; '));
167 | }
168 | this.setState(
169 | {
170 | selected: [],
171 | isLoading: false,
172 | },
173 | () => {
174 | this.refreshList();
175 | }
176 | );
177 | }
178 | } catch (error) {
179 | this.handleBackendError('Deletion failed:', error.message);
180 | }
181 | };
182 |
183 | handleBackendError = (title, message) => {
184 | this.setState({
185 | backendErrorTitle: title,
186 | backendErrorMessage: message,
187 | showBackendError: true,
188 | isLoading: false,
189 | });
190 | };
191 |
192 | handleErrorClose = () => {
193 | this.setState({
194 | showBackendError: false,
195 | isLoading: false,
196 | });
197 | };
198 |
199 | render() {
200 | const { classes } = this.props;
201 | const {
202 | policies,
203 | selected,
204 | order,
205 | isLoading,
206 | backendErrorTitle,
207 | backendErrorMessage,
208 | showBackendError,
209 | } = this.state;
210 |
211 | const rowCount = policies.length;
212 | const numSelected = selected.length;
213 |
214 | return (
215 |
216 |
217 |
226 |
235 |
247 |
257 |
258 |
265 |
266 |
267 |
268 |
269 | 0 && numSelected < rowCount}
271 | checked={rowCount > 0 && numSelected === rowCount}
272 | onChange={this.handleSelectAllClick}
273 | />
274 |
275 |
276 |
281 |
286 | Policies
287 |
288 |
289 |
290 |
291 |
292 |
293 | {map(policies, (policy) => {
294 | const isSelected = indexOf(selected, policy.name) !== -1;
295 | return (
296 |
304 |
305 | this.handleClick(event, policy)}
308 | />
309 |
310 |
311 |
312 |
318 | {policy.displayName || policy.name}
319 |
320 |
321 |
322 | );
323 | })}
324 |
325 |
326 |
327 |
328 | );
329 | }
330 | }
331 |
332 | export default compose(withRouter, withStyles(styles))(PolicyList);
333 |
--------------------------------------------------------------------------------
/client/src/pages/Schedule/ScheduleList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Recompose
4 | import { compose } from 'react-recompose';
5 |
6 | // Router
7 | import { withRouter } from 'react-router-dom';
8 |
9 | // Material UI
10 | import { withStyles } from '@material-ui/core/styles';
11 | import Table from '@material-ui/core/Table';
12 | import TableBody from '@material-ui/core/TableBody';
13 | import TableCell from '@material-ui/core/TableCell';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import TableSortLabel from '@material-ui/core/TableSortLabel';
17 | import Tooltip from '@material-ui/core/Tooltip';
18 | import Button from '@material-ui/core/Button';
19 | import Checkbox from '@material-ui/core/Checkbox';
20 | import AddIcon from '@material-ui/icons/Add';
21 | import RefreshIcon from '@material-ui/icons/Refresh';
22 | import EditIcon from '@material-ui/icons/Edit';
23 | import DeleteIcon from '@material-ui/icons/Delete';
24 |
25 | // Lodash
26 | import map from 'lodash/map';
27 | import indexOf from 'lodash/indexOf';
28 |
29 | // Project
30 | import ScheduleService from '../../modules/api/schedule';
31 | import AppPageContent from '../../modules/components/AppPageContent';
32 | import AppPageActions from '../../modules/components/AppPageActions';
33 |
34 | const styles = (theme) => ({
35 | root: {
36 | height: '100%',
37 | },
38 | button: {
39 | marginRight: theme.spacing(2),
40 | },
41 | leftIcon: {
42 | marginRight: theme.spacing(1),
43 | },
44 | link: {
45 | '&:hover': {
46 | textDecoration: 'underline',
47 | cursor: 'pointer',
48 | },
49 | },
50 | checkboxCell: {
51 | width: 48,
52 | },
53 | });
54 |
55 | class ScheduleList extends React.Component {
56 | constructor(props, context) {
57 | super(props, context);
58 | this.state = {
59 | schedules: [],
60 | selected: [],
61 | order: 'asc',
62 | isLoading: false,
63 | showBackendError: false,
64 | backendErrorTitle: null,
65 | backendErrorMessage: null,
66 | };
67 |
68 | this.scheduleService = new ScheduleService();
69 | }
70 |
71 | componentDidMount() {
72 | this.refreshList();
73 | }
74 |
75 | handleRequestSort = (event) => {
76 | this.setState((prevState, props) => {
77 | let order = 'desc';
78 | if (prevState.order === 'desc') {
79 | order = 'asc';
80 | }
81 |
82 | const schedules =
83 | order === 'desc'
84 | ? prevState.schedules.sort((a, b) =>
85 | b.displayname || b.name < a.displayname || a.name ? -1 : 1
86 | )
87 | : prevState.schedules.sort((a, b) =>
88 | a.displayname || a.name < b.displayname || b.name ? -1 : 1
89 | );
90 |
91 | return {
92 | schedules,
93 | order,
94 | };
95 | });
96 | };
97 |
98 | handleClickNavigate = (path) => (event) => {
99 | const { history } = this.props;
100 | history.push(path);
101 | };
102 |
103 | handleClickRefresh = (event) => {
104 | this.refreshList();
105 | };
106 |
107 | refreshList = async () => {
108 | this.setState({ isLoading: true });
109 | try {
110 | const schedules = await this.scheduleService.list();
111 | this.setState({
112 | schedules,
113 | isLoading: false,
114 | });
115 | } catch (error) {
116 | this.handleBackendError('Listing failed:', error.message);
117 | }
118 | };
119 |
120 | handleClick = (event, schedule) => {
121 | const { selected } = this.state;
122 | const selectedIndex = indexOf(selected, schedule.name);
123 | let newSelected = [];
124 |
125 | if (selectedIndex === -1) {
126 | newSelected = newSelected.concat(selected, schedule.name);
127 | } else if (selectedIndex === 0) {
128 | newSelected = newSelected.concat(selected.slice(1));
129 | } else if (selectedIndex === selected.length - 1) {
130 | newSelected = newSelected.concat(selected.slice(0, -1));
131 | } else if (selectedIndex > 0) {
132 | newSelected = newSelected.concat(
133 | selected.slice(0, selectedIndex),
134 | selected.slice(selectedIndex + 1)
135 | );
136 | }
137 |
138 | this.setState({ selected: newSelected });
139 | };
140 |
141 | handleSelectAllClick = (event, checked) => {
142 | if (checked) {
143 | this.setState({ selected: this.state.schedules.map((p) => p.name) });
144 | } else {
145 | this.setState({ selected: [] });
146 | }
147 | };
148 |
149 | handleDeleteClick = async (event) => {
150 | try {
151 | const { selected } = this.state;
152 | if (selected.length > 0) {
153 | const promises = [];
154 | this.setState({ isLoading: true });
155 | selected.forEach((schedule) => {
156 | promises.push(
157 | this.scheduleService.delete(schedule).catch((error) => error)
158 | );
159 | });
160 | const responses = await Promise.all(promises);
161 | const errorMessages = responses
162 | .filter((response) => response instanceof Error)
163 | .map((error) => error.message);
164 | if (errorMessages.length) {
165 | throw Error(errorMessages.join('; '));
166 | }
167 | this.setState(
168 | {
169 | selected: [],
170 | isLoading: false,
171 | },
172 | () => {
173 | this.refreshList();
174 | }
175 | );
176 | }
177 | } catch (error) {
178 | this.handleBackendError('Deletion failed:', error.message);
179 | }
180 | };
181 |
182 | handleBackendError = (title, message) => {
183 | this.setState({
184 | backendErrorTitle: title,
185 | backendErrorMessage: message,
186 | showBackendError: true,
187 | isLoading: false,
188 | });
189 | };
190 |
191 | handleErrorClose = () => {
192 | this.setState({
193 | showBackendError: false,
194 | isLoading: false,
195 | });
196 | };
197 |
198 | render() {
199 | const { classes } = this.props;
200 | const {
201 | schedules,
202 | selected,
203 | order,
204 | isLoading,
205 | backendErrorTitle,
206 | backendErrorMessage,
207 | showBackendError,
208 | } = this.state;
209 |
210 | const rowCount = schedules.length;
211 | const numSelected = selected.length;
212 |
213 | return (
214 |
215 |
216 |
225 |
234 |
246 |
256 |
257 |
258 |
265 |
266 |
267 |
268 |
269 | 0 && numSelected < rowCount}
271 | checked={rowCount > 0 && numSelected === rowCount}
272 | onChange={this.handleSelectAllClick}
273 | />
274 |
275 |
276 |
281 |
286 | Schedules
287 |
288 |
289 |
290 |
291 |
292 |
293 | {map(schedules, (schedule) => {
294 | const isSelected = indexOf(selected, schedule.name) !== -1;
295 | return (
296 |
304 |
305 | this.handleClick(event, schedule)}
308 | />
309 |
310 |
311 |
312 |
318 | {schedule.displayName || schedule.name}
319 |
320 |
321 |
322 | );
323 | })}
324 |
325 |
326 |
327 |
328 | );
329 | }
330 | }
331 |
332 | export default compose(withRouter, withStyles(styles))(ScheduleList);
333 |
--------------------------------------------------------------------------------
/docs/schedule_example.json:
--------------------------------------------------------------------------------
1 |
2 | Public
3 | {
4 | "Shape": [
5 | 7,
6 | 24
7 | ],
8 | "timezone": "Europe/Amsterdam",
9 | "Corder": true,
10 | "__ndarray__": [
11 | [
12 | 0,
13 | 1,
14 | 1,
15 | 1,
16 | 1,
17 | 1,
18 | 0,
19 | 0,
20 | 0,
21 | 0,
22 | 0,
23 | 0,
24 | 0,
25 | 0,
26 | 0,
27 | 0,
28 | 0,
29 | 0,
30 | 0,
31 | 0,
32 | 0,
33 | 0,
34 | 0,
35 | 0
36 | ],
37 | [
38 | 0,
39 | 0,
40 | 0,
41 | 0,
42 | 0,
43 | 0,
44 | 0,
45 | 0,
46 | 1,
47 | 1,
48 | 1,
49 | 1,
50 | 1,
51 | 1,
52 | 1,
53 | 1,
54 | 1,
55 | 1,
56 | 0,
57 | 0,
58 | 0,
59 | 0,
60 | 0,
61 | 0
62 | ],
63 | [
64 | 0,
65 | 0,
66 | 0,
67 | 0,
68 | 0,
69 | 0,
70 | 0,
71 | 0,
72 | 0,
73 | 0,
74 | 0,
75 | 1,
76 | 1,
77 | 0,
78 | 0,
79 | 0,
80 | 0,
81 | 0,
82 | 0,
83 | 0,
84 | 0,
85 | 0,
86 | 0,
87 | 0
88 | ],
89 | [
90 | 0,
91 | 0,
92 | 0,
93 | 0,
94 | 0,
95 | 0,
96 | 0,
97 | 0,
98 | 0,
99 | 0,
100 | 0,
101 | 0,
102 | 0,
103 | 0,
104 | 0,
105 | 0,
106 | 0,
107 | 0,
108 | 0,
109 | 0,
110 | 0,
111 | 0,
112 | 0,
113 | 0
114 | ],
115 | [
116 | 0,
117 | 0,
118 | 0,
119 | 0,
120 | 0,
121 | 0,
122 | 0,
123 | 0,
124 | 0,
125 | 0,
126 | 0,
127 | 0,
128 | 0,
129 | 0,
130 | 1,
131 | 1,
132 | 1,
133 | 1,
134 | 1,
135 | 0,
136 | 0,
137 | 0,
138 | 0,
139 | 0
140 | ],
141 | [
142 | 0,
143 | 0,
144 | 0,
145 | 0,
146 | 0,
147 | 0,
148 | 0,
149 | 0,
150 | 0,
151 | 0,
152 | 0,
153 | 0,
154 | 0,
155 | 0,
156 | 0,
157 | 0,
158 | 0,
159 | 0,
160 | 0,
161 | 0,
162 | 0,
163 | 0,
164 | 0,
165 | 0
166 | ],
167 | [
168 | 0,
169 | 0,
170 | 0,
171 | 0,
172 | 0,
173 | 0,
174 | 0,
175 | 0,
176 | 0,
177 | 0,
178 | 0,
179 | 0,
180 | 0,
181 | 0,
182 | 0,
183 | 0,
184 | 0,
185 | 0,
186 | 0,
187 | 0,
188 | 0,
189 | 0,
190 | 0,
191 | 0
192 | ]
193 | ],
194 | "dtype": "int64",
195 | "name": "schedule_name"
196 | }
197 | Zorya API Docs
198 |
199 | Zorya GCP Instance Scheduler
200 |
201 | Language
202 | POST Add a schedule
203 | http://localhost:8080/api/v1/add_schedule
204 | HEADERS
205 | Content-Typeapplication/json
206 | BODY
207 | {
208 | "Shape": [
209 | 7,
210 | 24
211 | ],
212 | "timezone": "Europe/Amsterdam",
213 | "Corder": true,
214 | "__ndarray__": [
215 | [
216 | 0,
217 | 1,
218 | 1,
219 | 1,
220 | 1,
221 | 1,
222 | 0,
223 | 0,
224 | 0,
225 | 0,
226 | 0,
227 | 0,
228 | 0,
229 | 0,
230 | 0,
231 | 0,
232 | 0,
233 | 0,
234 | 0,
235 | 0,
236 | 0,
237 | 0,
238 | 0,
239 | 0
240 | ],
241 | [
242 | 0,
243 | 0,
244 | 0,
245 | 0,
246 | 0,
247 | 0,
248 | 0,
249 | 0,
250 | 1,
251 | 1,
252 | 1,
253 | 1,
254 | 1,
255 | 1,
256 | 1,
257 | 1,
258 | 1,
259 | 1,
260 | 0,
261 | 0,
262 | 0,
263 | 0,
264 | 0,
265 | 0
266 | ],
267 | [
268 | 0,
269 | 0,
270 | 0,
271 | 0,
272 | 0,
273 | 0,
274 | 0,
275 | 0,
276 | 0,
277 | 0,
278 | 0,
279 | 1,
280 | 1,
281 | 0,
282 | 0,
283 | 0,
284 | 0,
285 | 0,
286 | 0,
287 | 0,
288 | 0,
289 | 0,
290 | 0,
291 | 0
292 | ],
293 | [
294 | 0,
295 | 0,
296 | 0,
297 | 0,
298 | 0,
299 | 0,
300 | 0,
301 | 0,
302 | 0,
303 | 0,
304 | 0,
305 | 0,
306 | 0,
307 | 0,
308 | 0,
309 | 0,
310 | 0,
311 | 0,
312 | 0,
313 | 0,
314 | 0,
315 | 0,
316 | 0,
317 | 0
318 | ],
319 | [
320 | 0,
321 | 0,
322 | 0,
323 | 0,
324 | 0,
325 | 0,
326 | 0,
327 | 0,
328 | 0,
329 | 0,
330 | 0,
331 | 0,
332 | 0,
333 | 0,
334 | 1,
335 | 1,
336 | 1,
337 | 1,
338 | 1,
339 | 0,
340 | 0,
341 | 0,
342 | 0,
343 | 0
344 | ],
345 | [
346 | 0,
347 | 0,
348 | 0,
349 | 0,
350 | 0,
351 | 0,
352 | 0,
353 | 0,
354 | 0,
355 | 0,
356 | 0,
357 | 0,
358 | 0,
359 | 0,
360 | 0,
361 | 0,
362 | 0,
363 | 0,
364 | 0,
365 | 0,
366 | 0,
367 | 0,
368 | 0,
369 | 0
370 | ],
371 | [
372 | 0,
373 | 0,
374 | 0,
375 | 0,
376 | 0,
377 | 0,
378 | 0,
379 | 0,
380 | 0,
381 | 0,
382 | 0,
383 | 0,
384 | 0,
385 | 0,
386 | 0,
387 | 0,
388 | 0,
389 | 0,
390 | 0,
391 | 0,
392 | 0,
393 | 0,
394 | 0,
395 | 0
396 | ]
397 | ],
398 | "dtype": "int64",
399 | "name": "schedule_name"
400 | }
401 |
402 |
403 | Sample Request
404 | Add a schedule
405 | curl --request POST \
406 | --url http://localhost:8080/api/v1/add_schedule \
407 | --header 'Content-Type: application/json' \
408 | --data '{
409 | "Shape": [
410 | 7,
411 | 24
412 | ],
413 | "timezone": "Europe/Amsterdam",
414 | "Corder": true,
415 | "__ndarray__": [
416 | [
417 | 0,
418 | 1,
419 | 1,
420 | 1,
421 | 1,
422 | 1,
423 | 0,
424 | 0,
425 | 0,
426 | 0,
427 | 0,
428 | 0,
429 | 0,
430 | 0,
431 | 0,
432 | 0,
433 | 0,
434 | 0,
435 | 0,
436 | 0,
437 | 0,
438 | 0,
439 | 0,
440 | 0
441 | ],
442 | [
443 | 0,
444 | 0,
445 | 0,
446 | 0,
447 | 0,
448 | 0,
449 | 0,
450 | 0,
451 | 1,
452 | 1,
453 | 1,
454 | 1,
455 | 1,
456 | 1,
457 | 1,
458 | 1,
459 | 1,
460 | 1,
461 | 0,
462 | 0,
463 | 0,
464 | 0,
465 | 0,
466 | 0
467 | ],
468 | [
469 | 0,
470 | 0,
471 | 0,
472 | 0,
473 | 0,
474 | 0,
475 | 0,
476 | 0,
477 | 0,
478 | 0,
479 | 0,
480 | 1,
481 | 1,
482 | 0,
483 | 0,
484 | 0,
485 | 0,
486 | 0,
487 | 0,
488 | 0,
489 | 0,
490 | 0,
491 | 0,
492 | 0
493 | ],
494 | [
495 | 0,
496 | 0,
497 | 0,
498 | 0,
499 | 0,
500 | 0,
501 | 0,
502 | 0,
503 | 0,
504 | 0,
505 | 0,
506 | 0,
507 | 0,
508 | 0,
509 | 0,
510 | 0,
511 | 0,
512 | 0,
513 | 0,
514 | 0,
515 | 0,
516 | 0,
517 | 0,
518 | 0
519 | ],
520 | [
521 | 0,
522 | 0,
523 | 0,
524 | 0,
525 | 0,
526 | 0,
527 | 0,
528 | 0,
529 | 0,
530 | 0,
531 | 0,
532 | 0,
533 | 0,
534 | 0,
535 | 1,
536 | 1,
537 | 1,
538 | 1,
539 | 1,
540 | 0,
541 | 0,
542 | 0,
543 | 0,
544 | 0
545 | ],
546 | [
547 | 0,
548 | 0,
549 | 0,
550 | 0,
551 | 0,
552 | 0,
553 | 0,
554 | 0,
555 | 0,
556 | 0,
557 | 0,
558 | 0,
559 | 0,
560 | 0,
561 | 0,
562 | 0,
563 | 0,
564 | 0,
565 | 0,
566 | 0,
567 | 0,
568 | 0,
569 | 0,
570 | 0
571 | ],
572 | [
573 | 0,
574 | 0,
575 | 0,
576 | 0,
577 | 0,
578 | 0,
579 | 0,
580 | 0,
581 | 0,
582 | 0,
583 | 0,
584 | 0,
585 | 0,
586 | 0,
587 | 0,
588 | 0,
589 | 0,
590 | 0,
591 | 0,
592 | 0,
593 | 0,
594 | 0,
595 | 0,
596 | 0
597 | ]
598 | ],
599 | "dtype": "int64",
600 | "name": "schedule_name"
601 | }'
602 | GET List schedules
603 | http://localhost:8080/api/v1/list_schedules
604 |
605 |
606 | Sample Request
607 | List schedules
608 | curl --request GET \
609 | --url http://localhost:8080/api/v1/list_schedules
610 | GET Delete schedule
611 | http://localhost:8080/api/v1/del_schedule?schedule=my schedule name
612 | PARAMS
613 | schedulemy schedule name
614 |
615 | Sample Request
616 | Delete schedule
617 | curl --request GET \
618 | --url 'http://localhost:8080/api/v1/del_schedule?schedule=my%20schedule%20name'
619 | GET Get schedule
620 | http://localhost:8080/api/v1/get_schedule?schedule=my schedule name
621 | PARAMS
622 | schedulemy schedule name
623 |
624 | Sample Request
625 | Get schedule
626 | curl --request GET \
627 | --url 'http://localhost:8080/api/v1/get_schedule?schedule=my%20schedule%20name'
628 | POST Add policy
629 | http://localhost:8080/api/v1/add_policy
630 | HEADERS
631 | Content-Typeapplication/json
632 | BODY
633 | {
634 | "name": "My policy",
635 | "tags": [
636 | {
637 | "dev": "sleeper",
638 | "staging": "resting"
639 | }
640 | ],
641 | "projetcs": [
642 | "project-x",
643 | "y-project"
644 | ],
645 | "schedulename": "my schedule name"
646 | }
647 |
648 |
649 | Sample Request
650 | Add policy
651 | curl --request POST \
652 | --url http://localhost:8080/api/v1/add_policy \
653 | --header 'Content-Type: application/json' \
654 | --data '{
655 | "name": "My policy",
656 | "tags": [
657 | {
658 | "dev": "sleeper",
659 | "staging": "resting"
660 | }
661 | ],
662 | "projetcs": [
663 | "project-x",
664 | "y-project"
665 | ],
666 | "schedulename": "my schedule name"
667 | }'
668 | GET Get policy
669 | http://localhost:8080/api/v1/get_policy?policy=My policy
670 | HEADERS
671 | Content-Typeapplication/json
672 | PARAMS
673 | policyMy policy
674 |
675 | Sample Request
676 | Get policy
677 | curl --request GET \
678 | --url 'http://localhost:8080/api/v1/get_policy?policy=My%20policy' \
679 | --header 'Content-Type: application/json'
680 | GET List policys
681 | http://localhost:8080/api/v1/list_policys
682 | HEADERS
683 | Content-Typeapplication/json
684 |
685 | Sample Request
686 | List policys
687 | curl --request GET \
688 | --url http://localhost:8080/api/v1/list_policys \
689 | --header 'Content-Type: application/json'
690 | GET Delete policy
691 | http://localhost:8080/api/v1/del_policy?policy=My policy
692 | HEADERS
693 | Content-Typeapplication/json
694 | PARAMS
695 | policyMy policy
696 |
697 | Sample Request
698 | Delete policy
699 | curl --request GET \
700 | --url 'http://localhost:8080/api/v1/del_policy?policy=My%20policy' \
701 | --header 'Content-Type: application/json'
702 | ZORYA API DOCS
703 |
704 | Introduction
705 | POST
706 | Add a schedule
707 | GET
708 | List schedules
709 | GET
710 | Delete schedule
711 | GET
712 | Get schedule
713 | POST
714 | Add policy
715 | GET
716 | Get policy
717 | GET
718 | List policys
719 | GET
720 | Delete policy
721 |
--------------------------------------------------------------------------------
/client/src/pages/Policy/Policy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Material UI
5 | import { withStyles } from '@material-ui/core/styles';
6 | import Typography from '@material-ui/core/Typography';
7 | import Button from '@material-ui/core/Button';
8 | import ArrowBackIcon from '@material-ui/icons/ArrowBack';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import TextField from '@material-ui/core/TextField';
11 | import InputLabel from '@material-ui/core/InputLabel';
12 | import FormGroup from '@material-ui/core/FormGroup';
13 | import FormControl from '@material-ui/core/FormControl';
14 | import Select from '@material-ui/core/Select';
15 |
16 | // Lodash
17 | import map from 'lodash/map';
18 | import find from 'lodash/find';
19 | import forOwn from 'lodash/forOwn';
20 |
21 | // Project
22 | import PolicyTags from '../../modules/components/PolicyTags';
23 | import AppPageContent from '../../modules/components/AppPageContent';
24 | import AppPageActions from '../../modules/components/AppPageActions';
25 | import PolicyService from '../../modules/api/policy';
26 | import ScheduleService from '../../modules/api/schedule';
27 | import { getDefaultPolicy } from '../../modules/utils/policy';
28 |
29 | const styles = (theme) => ({
30 | root: {
31 | height: '100%',
32 | },
33 | button: {
34 | marginRight: theme.spacing(2),
35 | },
36 | textField: {
37 | width: 550,
38 | marginBottom: theme.spacing(3),
39 | marginRight: theme.spacing(2),
40 | },
41 | formControl: {
42 | width: 550,
43 | marginBottom: theme.spacing(4),
44 | },
45 | });
46 |
47 | class Policy extends React.Component {
48 | constructor(props, context) {
49 | super(props, context);
50 | this.state = {
51 | policy: null,
52 | schedules: null,
53 | isLoading: false,
54 |
55 | nameError: false,
56 | scheduleError: false,
57 | projectsError: false,
58 | tagsError: [],
59 |
60 | showBackendError: false,
61 | backendErrorTitle: null,
62 | backendErrorMessage: null,
63 | };
64 |
65 | this.policyService = new PolicyService();
66 | this.scheduleService = new ScheduleService();
67 | }
68 |
69 | async componentDidMount() {
70 | try {
71 | const { match } = this.props;
72 | this.setState({ isLoading: true });
73 | const schedules = await this.scheduleService.list();
74 | if (!schedules || !schedules.length) {
75 | throw new Error('Create at least one Schedule first');
76 | }
77 |
78 | let policy;
79 | if (match.params.policy) {
80 | policy = await this.policyService.get(match.params.policy);
81 | } else {
82 | policy = getDefaultPolicy();
83 | if (schedules && schedules.length) {
84 | policy.schedulename = schedules[0].name;
85 | }
86 | }
87 | this.setState({
88 | policy,
89 | schedules,
90 | isLoading: false,
91 | });
92 | } catch (error) {
93 | this.handleBackendError(
94 | 'Loading Failed:',
95 | error.message,
96 | '/policies/browser'
97 | );
98 | }
99 | }
100 |
101 | handleChange = (name) => (event) => {
102 | const { policy } = this.state;
103 | policy[name] = event.target.value;
104 | this.setState({ policy });
105 | };
106 |
107 | handleChangeTags = (tags, shouldUpdateErrors) => {
108 | const { policy } = this.state;
109 | policy.tags = tags;
110 | if (shouldUpdateErrors) {
111 | const tagsError = this.getTagsError();
112 | this.setState({ policy, tagsError });
113 | } else {
114 | this.setState({ policy });
115 | }
116 | };
117 |
118 | handleChangeProjects = (event) => {
119 | const { policy } = this.state;
120 | policy.projects = event.target.value.replace(/\s/g, '').split(',');
121 | this.setState({
122 | policy,
123 | });
124 | };
125 |
126 | getTagsError = () => {
127 | const { policy } = this.state;
128 | const tagsKeyRe = /(^[a-z][a-z0-9_-]*[a-z0-9]$)|(^@app_engine_flex$)/; // Add app App Engine identifier
129 | const tagsValRe =
130 | /(^[a-z][a-z0-9_-]*[a-z0-9]$)|(^[a-z][a-z0-9_-]*:[a-z0-9_-]+$)/; // Add app App Engine identifier
131 | let tagsError = [];
132 | for (let i = 0; i < policy.tags.length; i++) {
133 | tagsError.push([false, false]);
134 | forOwn(policy.tags[i], (value, key) => {
135 | if (!tagsKeyRe.test(key)) {
136 | tagsError[i][0] = true;
137 | }
138 | if (!tagsValRe.test(value)) {
139 | tagsError[i][1] = true;
140 | }
141 | });
142 | }
143 | return tagsError;
144 | };
145 |
146 | handleSubmit = async (event) => {
147 | try {
148 | const { history } = this.props;
149 | const { policy } = this.state;
150 |
151 | const nameRe = /^[a-zA-Z][\w-]*[a-zA-Z0-9]$/;
152 | const projectsRe = /^[a-z][a-z0-9-]+[a-z0-9]$/;
153 |
154 | let nameError = false;
155 | let projectsError = !policy.projects.length;
156 | const scheduleError = !policy.schedulename;
157 | const tagsError = this.getTagsError();
158 |
159 | if (!nameRe.test(policy.name)) {
160 | nameError = true;
161 | }
162 |
163 | for (let i = 0; i < policy.projects.length && !projectsError; i++) {
164 | if (!projectsRe.test(policy.projects[i])) {
165 | projectsError = true;
166 | }
167 | }
168 |
169 | if (
170 | nameError ||
171 | projectsError ||
172 | scheduleError ||
173 | find(tagsError, (tagErrors) => tagErrors[0] || tagErrors[1])
174 | ) {
175 | this.setState({
176 | nameError,
177 | scheduleError,
178 | projectsError,
179 | tagsError,
180 | });
181 | } else {
182 | this.setState({ isLoading: true });
183 | await this.policyService.add(policy);
184 | this.setState({ isLoading: false });
185 | history.push('/policies/browser');
186 | }
187 | } catch (error) {
188 | this.handleBackendError('Update failed:', error.message);
189 | }
190 | };
191 |
192 | handleRequestCancel = (event) => {
193 | const { history } = this.props;
194 | history.goBack();
195 | };
196 |
197 | handleBackendError = (title, message, exitPage) => {
198 | this.setState({
199 | backendErrorTitle: title,
200 | backendErrorMessage: message,
201 | showBackendError: true,
202 | isLoading: false,
203 | exitPage,
204 | });
205 | };
206 |
207 | handleErrorClose = () => {
208 | const { history } = this.props;
209 | const { exitPage } = this.state;
210 | this.setState({
211 | showBackendError: false,
212 | isLoading: false,
213 | });
214 | if (exitPage) {
215 | history.push(exitPage);
216 | }
217 | };
218 |
219 | render() {
220 | const { classes, edit } = this.props;
221 | const {
222 | policy,
223 | schedules,
224 | nameError,
225 | scheduleError,
226 | projectsError,
227 | tagsError,
228 | isLoading,
229 | backendErrorTitle,
230 | backendErrorMessage,
231 | showBackendError,
232 | } = this.state;
233 |
234 | return (
235 |
236 |
237 |
242 |
243 |
244 |
245 | {edit ? (
246 |
247 | Edit policy {policy ? policy.name : ''}
248 |
249 | ) : (
250 |
251 | Create a policy
252 |
253 | )}
254 |
255 |
256 |
263 | {policy && (
264 |
265 |
266 |
281 |
282 |
294 |
295 |
296 |
301 | Schedule name
302 |
303 |
318 |
319 |
320 |
333 |
334 |
339 |
340 |
341 |
350 |
359 |
360 | )}
361 |
362 |
363 | );
364 | }
365 | }
366 |
367 | Policy.propTypes = {
368 | classes: PropTypes.object.isRequired,
369 | };
370 |
371 | export default withStyles(styles)(Policy);
372 |
--------------------------------------------------------------------------------
/client/src/modules/components/ScheduleTimeTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | // Material UI
6 | import { withStyles } from '@material-ui/core/styles';
7 | import Paper from '@material-ui/core/Paper';
8 | import Button from '@material-ui/core/Button';
9 |
10 | // Lodash
11 | import map from 'lodash/map';
12 | import find from 'lodash/find';
13 | import flatten from 'lodash/flatten';
14 |
15 | // react-lineto
16 | import { Line } from 'react-lineto';
17 |
18 | const days = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
19 | const hours = [...Array(24).keys()];
20 | const gutters = 4;
21 | const boxSize = 36;
22 |
23 | const getSelectionRect = (mouseDown, mouseUp) => {
24 | const selectionRect = {};
25 | if (mouseDown.x <= mouseUp.x) {
26 | selectionRect.left = mouseDown.x;
27 | selectionRect.right = mouseUp.x;
28 | if (mouseDown.y <= mouseUp.y) {
29 | selectionRect.top = mouseDown.y;
30 | selectionRect.bottom = mouseUp.y;
31 | } else {
32 | selectionRect.top = mouseUp.y;
33 | selectionRect.bottom = mouseDown.y;
34 | }
35 | } else {
36 | selectionRect.left = mouseUp.x;
37 | selectionRect.right = mouseDown.x;
38 | if (mouseDown.y <= mouseUp.y) {
39 | selectionRect.top = mouseDown.y;
40 | selectionRect.bottom = mouseUp.y;
41 | } else {
42 | selectionRect.top = mouseUp.y;
43 | selectionRect.bottom = mouseDown.y;
44 | }
45 | }
46 | return selectionRect;
47 | };
48 |
49 | const intersects = (rectA, rectB) => {
50 | return (
51 | rectA.left < rectB.right &&
52 | rectA.right > rectB.left &&
53 | rectA.top < rectB.bottom &&
54 | rectA.bottom > rectB.top
55 | );
56 | };
57 |
58 | const styles = (theme) => ({
59 | root: {
60 | margin: theme.spacing(2, 0),
61 | },
62 | row: {
63 | display: 'flex',
64 | flexDirection: 'row',
65 | },
66 | column: {
67 | display: 'flex',
68 | flexDirection: 'column',
69 | },
70 | hourBox: {
71 | height: boxSize,
72 | width: boxSize,
73 | margin: gutters,
74 | },
75 | columnButtonRoot: {
76 | minHeight: boxSize,
77 | minWidth: boxSize,
78 | margin: gutters,
79 | padding: 0,
80 | },
81 | rowButtonRoot: {
82 | minHeight: boxSize,
83 | minWidth: 48,
84 | margin: gutters,
85 | padding: 0,
86 | },
87 | boxContent: {
88 | display: 'flex',
89 | alignItems: 'center',
90 | justifyContent: 'center',
91 | },
92 | on: {
93 | backgroundColor: '#aed581',
94 | },
95 | off: {
96 | backgroundColor: '#eeeeee',
97 | },
98 | nextOn: {
99 | backgroundColor: '#7da453',
100 | },
101 | nextOff: {
102 | backgroundColor: '#bcbcbc',
103 | },
104 | crosshair: {
105 | cursor: 'crosshair',
106 | },
107 | });
108 |
109 | class ScheduleTimeTable extends React.Component {
110 | constructor(props, context) {
111 | super(props, context);
112 | this.state = {
113 | matrix: null,
114 | mouseDown: null,
115 | mouseCurrent: null,
116 | };
117 | }
118 |
119 | async componentDidMount() {
120 | try {
121 | const matrix = map(this.props.schedule.__ndarray__, (dayArray) =>
122 | map(dayArray, (hourValue) => ({ current: hourValue, next: null }))
123 | );
124 | this.setState({
125 | matrix,
126 | });
127 | } catch (ex) {
128 | console.error(ex);
129 | }
130 | }
131 |
132 | publishChanges = () => {
133 | const { matrix } = this.state;
134 | const ndarray = map(matrix, (day) => map(day, (hour) => hour.current));
135 | const schedule = {
136 | ...this.props.schedule,
137 | __ndarray__: ndarray,
138 | };
139 | this.props.onScheduleChange(schedule);
140 | };
141 |
142 | toggleDay = (dayIndex) => (event) => {
143 | this.setState(
144 | (prevState, props) => {
145 | const { matrix } = prevState;
146 | if (find(matrix[dayIndex], { current: 0 })) {
147 | matrix[dayIndex] = map(matrix[dayIndex], (hour) => ({
148 | current: 1,
149 | next: null,
150 | }));
151 | } else {
152 | matrix[dayIndex] = map(matrix[dayIndex], (hour) => ({
153 | current: 0,
154 | next: null,
155 | }));
156 | }
157 | return { matrix };
158 | },
159 | () => this.publishChanges()
160 | );
161 | };
162 |
163 | toggleHour = (hourIndex) => (event) => {
164 | this.setState(
165 | (prevState, props) => {
166 | const { matrix } = prevState;
167 | let hourIsOn = true;
168 | for (let i = 0; i < props.schedule.Shape[0]; i++) {
169 | hourIsOn = hourIsOn && !!matrix[i][hourIndex].current;
170 | }
171 | const newCurrent = hourIsOn ? 0 : 1;
172 | for (let i = 0; i < props.schedule.Shape[0]; i++) {
173 | matrix[i][hourIndex].current = newCurrent;
174 | }
175 | return { matrix };
176 | },
177 | () => this.publishChanges()
178 | );
179 | };
180 |
181 | toggleAll = (event) => {
182 | this.setState(
183 | (prevState, props) => {
184 | const matrix = prevState.matrix;
185 | const hasZero = find(flatten(matrix), { current: 0 });
186 | const newCurrent = hasZero ? 1 : 0;
187 | for (let dayIndex = 0; dayIndex < props.schedule.Shape[0]; dayIndex++) {
188 | for (
189 | let hourIndex = 0;
190 | hourIndex < props.schedule.Shape[1];
191 | hourIndex++
192 | ) {
193 | matrix[dayIndex][hourIndex].current = newCurrent;
194 | }
195 | }
196 | return {
197 | matrix,
198 | };
199 | },
200 | () => this.publishChanges()
201 | );
202 | };
203 |
204 | getDayGrid = (dayIndex, dayValues) => {
205 | const { classes } = this.props;
206 |
207 | const hourBlocks = map(dayValues, (hour, hourIndex) => (
208 |
209 | {hour.next !== null ? (
210 |
217 | ) : (
218 |
225 | )}
226 |
227 | ));
228 |
229 | return hourBlocks;
230 | };
231 |
232 | handleMouseMove = (event) => {
233 | const { mouseDown, newCurrent } = this.state;
234 | const matrix = this.state.matrix.slice();
235 | const matrixRect = this.matrixDiv.getBoundingClientRect();
236 |
237 | const mouseCurrent = {
238 | x: event.clientX,
239 | y: event.clientY,
240 | };
241 |
242 | if (mouseCurrent.x < matrixRect.left) {
243 | mouseCurrent.x = matrixRect.left;
244 | }
245 | if (mouseCurrent.x > matrixRect.right) {
246 | mouseCurrent.x = matrixRect.right;
247 | }
248 | if (mouseCurrent.y < matrixRect.top) {
249 | mouseCurrent.y = matrixRect.top;
250 | }
251 | if (mouseCurrent.y > matrixRect.bottom) {
252 | mouseCurrent.y = matrixRect.bottom;
253 | }
254 |
255 | const selectionRect = getSelectionRect(mouseDown, mouseCurrent);
256 |
257 | for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
258 | for (let hourIndex = 0; hourIndex < 24; hourIndex++) {
259 | const top =
260 | matrixRect.top +
261 | gutters +
262 | dayIndex * boxSize +
263 | dayIndex * gutters * 2;
264 | const left =
265 | matrixRect.left +
266 | gutters +
267 | hourIndex * boxSize +
268 | hourIndex * gutters * 2;
269 | const hourRect = {
270 | top,
271 | left,
272 | bottom: top + boxSize,
273 | right: left + boxSize,
274 | };
275 | if (intersects(hourRect, selectionRect)) {
276 | matrix[dayIndex][hourIndex].next = newCurrent;
277 | } else {
278 | matrix[dayIndex][hourIndex].next = null;
279 | }
280 | }
281 | }
282 |
283 | this.setState({
284 | matrix,
285 | mouseCurrent,
286 | });
287 | };
288 |
289 | handleMouseDown = (event) => {
290 | event.persist();
291 | if (event.button === 0) {
292 | const { matrix } = this.state;
293 |
294 | const mouseDown = {
295 | x: event.clientX,
296 | y: event.clientY,
297 | };
298 |
299 | const start = getSelectionRect(mouseDown, mouseDown);
300 | const matrixRect = this.matrixDiv.getBoundingClientRect();
301 | for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
302 | for (let hourIndex = 0; hourIndex < 24; hourIndex++) {
303 | const top = matrixRect.top + dayIndex * (boxSize + gutters * 2);
304 | const left = matrixRect.left + hourIndex * (boxSize + gutters * 2);
305 | const hourRect = {
306 | top,
307 | left,
308 | bottom: top + boxSize + gutters * 2,
309 | right: left + boxSize + gutters * 2,
310 | };
311 | if (intersects(hourRect, start)) {
312 | const newCurrent = matrix[dayIndex][hourIndex].current ? 0 : 1;
313 | document.addEventListener('mousemove', this.handleMouseMove, false);
314 | document.addEventListener('mouseup', this.handleMouseUp, false);
315 | this.setState({
316 | mouseDown,
317 | newCurrent,
318 | });
319 | }
320 | }
321 | }
322 | }
323 | };
324 |
325 | handleMouseUp = (event) => {
326 | const { mouseDown } = this.state;
327 | document.removeEventListener('mouseup', this.handleMouseUp, false);
328 | document.removeEventListener('mousemove', this.handleMouseMove, false);
329 | if (mouseDown) {
330 | const mouseUp = {
331 | x: event.clientX,
332 | y: event.clientY,
333 | };
334 |
335 | const selectionRect = getSelectionRect(mouseDown, mouseUp);
336 | this.toggleSelectionBox(selectionRect);
337 | }
338 |
339 | this.setState({
340 | mouseDown: null,
341 | mouseCurrent: null,
342 | });
343 | };
344 |
345 | toggleSelectionBox = (selectionRect) => {
346 | const { newCurrent } = this.state;
347 | const matrix = this.state.matrix.slice();
348 | const matrixRect = this.matrixDiv.getBoundingClientRect();
349 |
350 | for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
351 | for (let hourIndex = 0; hourIndex < 24; hourIndex++) {
352 | const top =
353 | matrixRect.top +
354 | gutters +
355 | dayIndex * boxSize +
356 | dayIndex * gutters * 2;
357 | const left =
358 | matrixRect.left +
359 | gutters +
360 | hourIndex * boxSize +
361 | hourIndex * gutters * 2;
362 | const hourRect = {
363 | top,
364 | left,
365 | bottom: top + boxSize,
366 | right: left + boxSize,
367 | };
368 | if (intersects(hourRect, selectionRect)) {
369 | matrix[dayIndex][hourIndex].current = newCurrent;
370 | matrix[dayIndex][hourIndex].next = null;
371 | }
372 | }
373 | }
374 |
375 | this.setState(
376 | {
377 | matrix,
378 | },
379 | () => this.publishChanges()
380 | );
381 | };
382 |
383 | render() {
384 | const { classes } = this.props;
385 | const { matrix, mouseDown, mouseCurrent } = this.state;
386 |
387 | return (
388 | matrix && (
389 |
390 |
391 |
399 | {map(hours, (hour) => (
400 |
401 |
410 |
411 | ))}
412 |
413 |
414 |
415 |
416 | {map(days, (day, dayIndex) => (
417 |
427 | ))}
428 |
429 |
430 |
(this.matrixDiv = matrixDiv)}
433 | onMouseDown={this.handleMouseDown}
434 | >
435 | {mouseDown && mouseCurrent && (
436 |
437 |
444 |
451 |
458 |
465 |
466 | )}
467 |
468 | {map(matrix, (dayValues, dayIndex) => (
469 |
470 | {this.getDayGrid(dayIndex, dayValues)}
471 |
472 | ))}
473 |
474 |
475 |
476 | )
477 | );
478 | }
479 | }
480 |
481 | ScheduleTimeTable.propTypes = {
482 | classes: PropTypes.object.isRequired,
483 | };
484 |
485 | export default withStyles(styles)(ScheduleTimeTable);
486 |
--------------------------------------------------------------------------------