├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── api └── api.go ├── conf ├── config.json └── vision.service ├── core ├── apiDoc │ └── apiDoc.go ├── fileDriver │ └── fileDriver.go ├── fileHandler │ ├── head.go │ ├── head_test.go │ ├── tail.go │ └── tail_test.go ├── hostInfoDriver │ └── hostInfoDriver.go ├── hostInfoHandler │ └── hostInfoHandler.go ├── models │ ├── configModel.go │ ├── hostInfoModel.go │ ├── model.go │ ├── procModels.go │ ├── sysMetricModel.go │ └── systemdModel.go ├── procDriver │ └── procDriver.go ├── procHandler │ └── procHandler.go ├── sysMetricDriver │ └── sysMetricDriver.go ├── sysMetrichandler │ └── sysMetrichandler.go ├── systemdDriver │ └── systemdDriver.go ├── systemdHandler │ └── systemdHandler.go └── util │ ├── checkAcls.go │ ├── checkAcls_test.go │ ├── logutil.go │ ├── util.go │ └── util_test.go └── main └── vision.go /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.12.4 4 | 5 | script: 6 | cp -r $GOPATH/src/github.com/LazyWolves/vision $GOPATH/src && 7 | cd $GOPATH/src/vision && 8 | make && 9 | make test 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Vision 2 | 3 | If you fing any bug in Vision or have an idea for a cool feature, please create a new issue. 4 | If it is a bug then the title should be prefixed [BUG]. Please see the below template for filing a bug. 5 | 6 | Template for BUG 7 | 8 | ``` 9 | [BUG] Short description of the bug (one liner) 10 | 11 | What is happening : Description of the bug 12 | 13 | What is expected : What should be the desired behaviour 14 | 15 | Steps to Reproduce : How to reproduce the bug 16 | 17 | Suggested Fix : How are you planning to fix the bug, if you plan to fix it at all. 18 | 19 | ``` 20 | 21 | The issue must comply with the above template. 22 | 23 | For feature request, please follow the below template 24 | 25 | ``` 26 | [Feature] Short description of the desired feature (one liner) 27 | 28 | A detailed description of the feature 29 | 30 | How are you planning to implement it, if you want to take it up. 31 | 32 | ``` 33 | If you want to work on an Issue, please comment on that issue so that someone assigns you to that issue. 34 | Once you are assigned to that issue you can proceed with hacking vision and sent a Pull Request. 35 | 36 | All pull requests must be made against the **dev (default)** branch. 37 | 38 | Pull requests should comply the below mentioned template : 39 | 40 | ``` 41 | #Issue-number : What it fixes (one liner) 42 | 43 | Detailed description of what the Pull request is about and what it does 44 | 45 | ``` 46 | 47 | While sending the PR, please make sure your commits are squashed. The commit message header should contain the corresponding issue number, 48 | like 49 | 50 | ``` Fixed #[issue_no] : Commit message header ``` 51 | 52 | The commit message body should describe in detail what has been done as a part of the PR. 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GODOC=godoc 3 | GOBUILD=$(GOCMD) build 4 | GORUN=$(GOCMD) run 5 | GOCLEAN=$(GOCMD) clean 6 | GOTEST=$(GOCMD) test 7 | BINARY_NAME=vision 8 | DIST_DIR=dist 9 | SRC=main/vision.go 10 | CONF_DIR=conf 11 | prefix=/usr 12 | 13 | all: build 14 | 15 | build: clean 16 | @echo "building vision..." 17 | $(GOCMD) get ./main/ 18 | @mkdir $(DIST_DIR) 19 | $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME) $(SRC) 20 | @echo "build successful" 21 | 22 | clean: 23 | $(GOCLEAN) 24 | @rm -rf $(DIST_DIR) 25 | @echo "build cleaned" 26 | 27 | run: 28 | $(GORUN) $(SRC) 29 | 30 | install: 31 | @install -D $(DIST_DIR)/$(BINARY_NAME) $(DESTDIR)$(prefix)/bin/$(BINARY_NAME) 32 | @mkdir -p $(DESTDIR)/etc/$(BINARY_NAME) 33 | @mkdir -p $(DESTDIR)/etc/systemd/system 34 | @cp $(CONF_DIR)/config.json $(DESTDIR)/etc/vision/config.json 35 | @cp $(CONF_DIR)/vision.service $(DESTDIR)/etc/systemd/system/vision.service 36 | @echo "installed vision" 37 | 38 | test: 39 | $(GOTEST) ./... 40 | 41 | uninstall: 42 | @rm -f $(DESTDIR)$(prefix)/bin/$(BINARY_NAME) 43 | @rm -rf $(DESTDIR)/etc/vision 44 | @rm -rf $(DESTDIR)/etc/systemd/system/vision.service 45 | @echo "uinsalled vision" 46 | 47 | doc: 48 | @$(GODOC) -http=:6060 49 | 50 | .PHONY: all build clean run install uninstall 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/LazyWolves/vision.svg?branch=dev)](https://travis-ci.org/LazyWolves/vision) 2 | 3 | ## Vision 4 | 5 | Vision is a light weight tool written purely in golang for viewing and fetching information on your system's state remotely. Vision allows you to view 6 | config files, log files and other such files over HTTP via your browser or on your terminal. It allows you to set ACLs via 7 | which you can block view on certain resources and alow view on certain resources. It allows you to configure aliases 8 | so that you do not have to type the entire path of the resource on server, view a file from top, or bottom, apply regex 9 | for filtering contents and specify number of lines to be read form desired files. 10 | 11 | Apart from viewing file resources it also allows you to view information about your remote host, processes running and their state and information on them, system metrcis like CPU and Memory, status of your systemd services running and option to 12 | start and stop them. 13 | 14 | ## Features and use cases 15 | 16 | ### Viewing remote resource files 17 | 18 | - Viewing resources (log files, config files) on remote servers over http. 19 | 20 | - During debugging, when multiple files has to be viewed in different servers, vision can be used to view such files 21 | on browser or terminal without ssh'ing into all the servers. 22 | 23 | - Vision allows line limit, reading from head and tail (equivalent to **head** and **tail** in linux), and applying 24 | regex to filter content (similar to **grep** but limited) 25 | 26 | - A sysad might not want all resources to be viewed. To address this, vision allows ACLs. You can define simple ACLs like 27 | allow_all, allow_for, block_for, via which you can allow certain files to be read or blocked from reading or you can block 28 | a directory altogether. 29 | 30 | ### Fetching remote Host information 31 | 32 | - Vision allows you to view basic informations about your remote hosts over HTTP 33 | - Host information includes ```hostname```, ```uptime```, ```bootTime```, ```Porcs```, ```OS```, ```Platform```, ```Arch```, 34 | ```KernelVersion```, ```Virtualisation Type```, ```Virtualisation Role```, etc. 35 | 36 | ### Fetching System Metrics from your remote system 37 | 38 | - Vision allows you to view systems metrics corresponding to your rempte host 39 | - As of now it allows you to fetch ```CPU utilisation``` and ```memory utilisation```. 40 | 41 | ### Fetching process information 42 | 43 | - Vision allows you to view all the processes running presently on your remote system. 44 | - You can query details of a process via its PID. 45 | - For a queried process it shows ```Name```, ```command line arguments```, ```executable path```, 46 | ```current working directory```, ```Status```, ```Number of Threads```, ```GIDS and UIDS```, etc. 47 | 48 | ### Fetching systemd services information 49 | 50 | - Vision allows you to view the ```status``` of the various systemd services running in your remote system 51 | - It allows you to list all the systemd services running in your system, and also allows you to filter via name. 52 | - You can also ```start``` or ```stop``` a service via vision remotely over HTTP. 53 | 54 | ## Getting started 55 | 56 | ## Installing from debian package 57 | 58 | ### NOTE : The debian package does not yet support querying host info, process info, systemd info and system metric info. These features will be included in the package in the next release. 59 | 60 | Get the debian package using the following: 61 | 62 | ``` 63 | wget -O vision_0.1-1_amd64.deb https://github.com/djmgit/vision_debian/blob/master/vision_0.1-1_amd64.deb?raw=true 64 | 65 | ``` 66 | 67 | Once the package is downloaded, installing the package using the following 68 | 69 | ``` 70 | sudo dpkg -i vision_0.1-1_amd64.deb 71 | 72 | ``` 73 | 74 | ## Building from source (for Debian based systems) 75 | 76 | You must have go binaries installed on your system and GOPATH and GOROOT set for bulding from source. 77 | 78 | Get the repository using the following go command 79 | ``` 80 | go get github.com/djmgit/vision/main 81 | 82 | ``` 83 | Next enter into vision directory containing the source (you will find it in your GOPATH i,e, go/src/github.com/djmgit/vision) 84 | and then execute the following make commands 85 | 86 | ``` 87 | make 88 | sudo make install 89 | 90 | ``` 91 | The first command will build vision. 92 | 93 | The second make command installs the vision binary on your system. 94 | 95 | ### Building DOC locally 96 | 97 | To build the HTML version of the doc and serve locally, execute the following in the repo folder 98 | 99 | ``` 100 | 101 | make doc 102 | 103 | ``` 104 | Then hit ```http://127.0.0.1:6060/pkg/github.com/djmgit/vision/``` on the browser to view the documentaion of the source code. 105 | 106 | ## Post Installation : Testing it out 107 | 108 | Once vision is installed on your system, you need to start the service using the following 109 | 110 | ``` 111 | sudo systemctl start vision 112 | 113 | ``` 114 | Vision assumes you use systemd for managing your services. Otherwise you can run vision using : 115 | 116 | ``` 117 | /usr/bin/vision 118 | ``` 119 | 120 | By default vision listens on port 8080, however you can change that (see below in configuring vision section) 121 | 122 | To verify that vision was installed properly and is working as expected, hit the following URL on browser : 123 | 124 | ``` 125 | [server_ip]:8080/path?=[path_to_a_file_on_your_system] 126 | ``` 127 | You should be able to view the last 10 lines of the file you have mentioned in the path parameter (provided its a valid paht) 128 | 129 | ## Uninstalling Vision 130 | 131 | First stop the service using : 132 | 133 | ``` 134 | sudo systemctl stop vision 135 | ``` 136 | 137 | Then uninsall and clean your build using the following (execute them in the vision source folder): 138 | 139 | ``` 140 | sudo make uninstall 141 | make clean 142 | ``` 143 | 144 | The above will uninstall vision, remove service and config files and clean the build (remove the dist directory containing 145 | vision binary) 146 | 147 | ## Configuring Vision 148 | 149 | The config file for vision will be present at /etc/vision/config.json 150 | As you have already guessed, the config file is written in json format. 151 | 152 | Given below is a sample config file 153 | 154 | ```{ 155 | "port": 8080, 156 | "allow_all": true, 157 | "block_for": [ 158 | "path1", 159 | "path2", 160 | "dir1", 161 | "dir2" 162 | ], 163 | "allow_for": [ 164 | "" 165 | ], 166 | "aliases": [ 167 | { 168 | "alias_name" : "apache2", 169 | "alias_to" : "/var/log/apache2/access.log", 170 | }, 171 | { 172 | "alias_name" : "kafka" 173 | "alias_to" : "/var/log/kafka/server.log" 174 | } 175 | ] 176 | } 177 | 178 | ``` 179 | 180 | Below are the description of each field : 181 | 182 | - **port** : it is the port on which vision will listen. Make sure the port you are specifying over here is free 183 | 184 | - **allow_all** : If true, all the files in your system can be view via vision except those present in the block_for 185 | list. Its value can be true or false. If false, no file in your system can be viewed via vision, except 186 | those present in allow_for list. 187 | 188 | - **allow_for** : Takes a list of paths. When **allow_all** is **false**, only the file paths present in this list will be 189 | visible via vision. The list may also contain a directory path. In this case, all the files in that directory 190 | will be visible. 191 | 192 | - **block_for** : Takes a list of paths. When **allow_all** is **true**, the file paths present in this list will not be visible 193 | via vision. The list may also contain a directory path. In that case, all the files in that directory will 194 | be unaccessable via vision. 195 | 196 | - **aliases** : Takes a list of objects. An object contains two keys - **alias_name** and **alias_to**. alias_name is the 197 | name of the alias (basically a short name for a long path). Once a alias has been added, the corresponding 198 | resource can be queried using the alias without providing the full path all the time. 199 | 200 | It is to be noted that allow_for is used when allow_all is set to **false** and block_for is used when allow_all is set to 201 | **true**. Providing value for both allow_for and block_for will not have any affect. 202 | 203 | ## Usage and API endpoints 204 | 205 | The following section describes how to use Vision endpoints in order to use its various features. 206 | 207 | ### Viewing remote resource files 208 | 209 | The Base path is [server_ip]:[port]/ 210 | 211 | All the options are simply passed as URL params. Only GET method is required. 212 | 213 | Following are the options that can be used with vision : 214 | 215 | | Param | Type | Description | 216 | |:----------------|:-----------------|:-----------------------| 217 | | path | String | Absolute path of the resource file on the remote system| 218 | | readFrom | String | Specifies from where to read the file, can be wither of head ot tail| 219 | | limit | Integer | Specifies the number of lines to be read| 220 | | filterBy | String (regex) | A regex to filter out desired lines from the given file. Only thoe lines containing patterns matched by the given regex will be returned.| 221 | | ignore | String (regex) | A regex to exclude lines containing patterns matching the regex | 222 | | alias | String | An alias name. Must be configured beforehand | 223 | 224 | Some examples : 225 | 226 | ``` 227 | The following will return the last 10 lines of apache access log. If limit and readFrom is not mentioned, by default 228 | readFrom is tail and limit is 10. 229 | 230 | http://[server-ip]:[port]?path=/var/log/apache2/access.log 231 | 232 | The following will return the first 100 lines of apache access log 233 | 234 | http://[server-ip]:[port]?path=/var/log/apache2/access.log&readFrom=head&limit=100 235 | 236 | The following will return only those lines which contain the word INFO from kafka log 237 | 238 | http://[server-ip]:[port]?path=/var/log/kafka/server.log&readFrom=head&limit=100&filterBy=INFO 239 | 240 | The following will return only those lines which does not contain the word WARNING from kafka log 241 | 242 | http://[server-ip]:[port]?path=/var/log/kafka/server.log&readFrom=head&limit=100&ignore=WARNING 243 | 244 | The following will read the first 20 lines of the file aliases by 'nginx' 245 | 246 | http://[server-ip]:[port]?alias=nginx&readFrom=head&limit=100&ignore=WARNING 247 | 248 | ``` 249 | 250 | ### Viewing remote host information 251 | 252 | The Base path is [server_ip]:[port]/hostInfo 253 | 254 | Example: 255 | 256 | ``` 257 | 258 | API call : http://[server-ip]:[port]:8080/hostInfo 259 | 260 | Response: 261 | 262 | { 263 | "HostInfo": { 264 | "hostname": "vision-test", 265 | "uptime": 6747, 266 | "bootTime": 1580965085, 267 | "procs": 322, 268 | "os": "linux", 269 | "platform": "ubuntu", 270 | "platformFamily": "debian", 271 | "platformVersion": "18.04", 272 | "kernelVersion": "4.15.0-76-generic", 273 | "kernelArch": "x86_64", 274 | "virtualizationSystem": "kvm", 275 | "virtualizationRole": "host", 276 | "hostid": "4bdc21cc-2895-11b2-a85c-9a4598d18182" 277 | }, 278 | "Timestamp": 1580971832, 279 | "TimestampUTC": "2020-02-06 06:50:32.614724886 +0000 UTC" 280 | } 281 | 282 | ``` 283 | 284 | ### Fetching System Metrics from your remote system 285 | 286 | The Base path is [server_ip]:[port]/systemMetrics 287 | 288 | Example: 289 | 290 | ``` 291 | API call : http://[serve_ip]:[port]/systemMetrics 292 | 293 | Response: 294 | 295 | { 296 | "Metrics": { 297 | "CPU": { 298 | "LoadAvg": { 299 | "Load1": 0.77, 300 | "Load5": 0.66, 301 | "Load15": 0.57 302 | } 303 | }, 304 | "Memory": { 305 | "VirtualMemory": { 306 | "MemTotal": 16329969664, 307 | "MemFree": 9131687936, 308 | "UsedPercent": 23.65463206288538 309 | } 310 | } 311 | }, 312 | "Timestamp": 1580972115, 313 | "TimestampUTC": "2020-02-06 06:55:15.481317283 +0000 UTC" 314 | } 315 | 316 | ``` 317 | 318 | ### Fetching process information 319 | 320 | The Base path is [server_ip]:[port]/procs 321 | 322 | All the options are passed as URL params. Only GET method is required. 323 | 324 | | Param | Type | Description | 325 | |:----------------|:-----------------|:-----------------------| 326 | | pid | int | Pid of the desired process to be queried| 327 | 328 | Example: 329 | 330 | ``` 331 | API call : http://[server_ip]:[port]/procs 332 | 333 | Response: 334 | 335 | { 336 | "ProcList": [ 337 | { 338 | "Pid": 0, 339 | "Name": "", 340 | "CmdLine": "" 341 | }, 342 | { 343 | "Pid": 0, 344 | "Name": "", 345 | "CmdLine": "" 346 | }, 347 | { 348 | "Pid": 1, 349 | "Name": "systemd", 350 | "CmdLine": "/sbin/init splash" 351 | }, 352 | { 353 | "Pid": 2, 354 | "Name": "kthreadd", 355 | "CmdLine": "" 356 | } 357 | ], 358 | "Timestamp": 1580972613, 359 | "TimestampUTC": "2020-02-06 07:03:33.870976948 +0000 UTC" 360 | } 361 | 362 | API call : http://[server_ip]:[port]/procs?pid=1234 363 | 364 | Response: 365 | 366 | { 367 | "ProcDesc": { 368 | "Pid": 1234, 369 | "Ppid": 1, 370 | "Name": "mongod", 371 | "CmdLine": "/usr/bin/mongod --config /etc/mongod.conf", 372 | "ExePath": "/usr/bin/mongod", 373 | "Cwd": "/", 374 | "Status": "S", 375 | "Uids": [ 376 | 122, 377 | 122, 378 | 122, 379 | 122 380 | ], 381 | "Gids": [ 382 | 127, 383 | 127, 384 | 127, 385 | 127 386 | ], 387 | "Nice": 20, 388 | "NumThreads": 26 389 | }, 390 | "Timestamp": 1580972908, 391 | "TimestampUTC": "2020-02-06 07:08:28.947810077 +0000 UTC" 392 | } 393 | 394 | ``` 395 | 396 | ### Fetching systemd services information 397 | 398 | The Base path is [server_ip]:[port]/systemd 399 | 400 | | Param | Type | Description | 401 | |:----------------|:-----------------|:-----------------------| 402 | | operation | string | Expected values are **start**, **stop** and **list**. list will list the systemd services running, start will start a given systemd service, and stop will stop the service| 403 | | filterBy | string | Excepts a regex, will list systemd services whose name matches the given regex, should be used with operation=list | 404 | | serviceName | string | Excepts a service name, used to specify service to be started or stopped, should be used with operation=start|stop | 405 | 406 | Example : 407 | 408 | ``` 409 | The following API call can be used to list all systemd services running: 410 | 411 | http://[server_ip]:[port]/systemd?operation=lisy&filterBy=*.service 412 | 413 | Response: 414 | 415 | { 416 | "Services": [ 417 | { 418 | "ServiceName": "festival.service", 419 | "ServiceState": "inactive" 420 | }, 421 | { 422 | "ServiceName": "user@1000.service", 423 | "ServiceState": "active" 424 | }, 425 | { 426 | "ServiceName": "openvpn.service", 427 | "ServiceState": "active" 428 | }, 429 | { 430 | "ServiceName": "ModemManager.service", 431 | "ServiceState": "active" 432 | }, 433 | { 434 | "ServiceName": "sssd.service", 435 | "ServiceState": "inactive" 436 | }, 437 | { 438 | "ServiceName": "unattended-upgrades.service", 439 | "ServiceState": "inactive" 440 | }, 441 | { 442 | "ServiceName": "systemd-fsck-root.service", 443 | "ServiceState": "inactive" 444 | }, 445 | { 446 | "ServiceName": "kerneloops.service", 447 | "ServiceState": "active" 448 | }, 449 | { 450 | "ServiceName": "console-setup.service", 451 | "ServiceState": "active" 452 | } 453 | ], 454 | "NumServices": 131, 455 | "Timestamp": 1580974430, 456 | "TimestampUTC": "2020-02-06 07:33:50.024080951 +0000 UTC" 457 | } 458 | 459 | The following API call can be used to stop haproxy, (if haproxy is running in the system, just an example) 460 | 461 | API call : http://[server_ip]:[port]/systemd?operation=stopt&serviceName=haproxy 462 | 463 | Response: 464 | 465 | { 466 | "Status": "OK", 467 | "Timestamp": 1580974652, 468 | "TimestampUTC": "2020-02-06 07:37:32.048756127 +0000 UTC" 469 | } 470 | 471 | Similarly the following can be used to start it back 472 | 473 | API call : http://[server_ip]:[port]/systemd?operation=start&serviceName=haproxy 474 | 475 | Response: 476 | 477 | { 478 | "Status": "OK", 479 | "Timestamp": 1580974704, 480 | "TimestampUTC": "2020-02-06 07:38:24.708853208 +0000 UTC" 481 | } 482 | 483 | ``` 484 | 485 | ## Contributing to Vision 486 | 487 | All opensource enthusiasts and Golang lovers are most welcome to add more features and fix bugs in vision. 488 | 489 | Please have a look at CONTRIBUTING.md 490 | 491 | 492 | 493 | 494 | 495 | 496 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // api package contains the main api endpoints which allows users to view resources 2 | package api 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "time" 12 | "strconv" 13 | "strings" 14 | "vision/core/fileDriver" 15 | "vision/core/sysMetricDriver" 16 | "vision/core/procDriver" 17 | "vision/core/hostInfoDriver" 18 | "vision/core/systemdDriver" 19 | "vision/core/models" 20 | "vision/core/util" 21 | "vision/core/apiDoc" 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // configJson struct is the model for storing and holding the config data 26 | var configJson models.ConfigModel 27 | 28 | // Logger object for writing logs to file 29 | var logger *logrus.Logger 30 | 31 | // aliases is a hashmap to create a one to one mapping 32 | // between the alias name and resource path 33 | var aliases map[string]string 34 | 35 | // Store path to config file 36 | var configJsonPath = "/etc/vision/config.json" 37 | 38 | // Store path to log file 39 | var logFilePath = "/var/log/vision/vision.log" 40 | 41 | // Main function which loads config, creates alias hash and attaches handlers to routes 42 | func Api() { 43 | // Read the config file and load it into memory as json 44 | // The connfig json will be stored in memory throughout the life 45 | // time of the object for fast retrieval of config 46 | loadConfigJson() 47 | 48 | // Create the alias map for fast retrieval. 49 | // The map will be stored in memory all the time 50 | // to allow repeated file access 51 | createAliasMap() 52 | 53 | // Open log file for logging purpose and initialise logger object 54 | fileHandler, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 55 | 56 | // CLose the log file before the function returns 57 | defer fileHandler.Close() 58 | 59 | // Get an instance of logger 60 | logger = util.SetupLogger() 61 | 62 | // Notify user if log file could not be opened 63 | if err != nil { 64 | logger.Warning("Could not open log file : ", logFilePath) 65 | } else { 66 | 67 | // If the log file could be loaded then use it in logger object 68 | logger.SetOutput(fileHandler) 69 | } 70 | 71 | // Create route for / path and add handler function for it 72 | http.HandleFunc("/", apiHandler) 73 | 74 | // Create route for /aliases path and add handler function to it 75 | http.HandleFunc("/aliases", aliasHandler) 76 | http.HandleFunc("/apiDoc", apiDocHandler) 77 | http.HandleFunc("/systemMetrics", sysMetricApihandler) 78 | http.HandleFunc("/procs", procApiHandler) 79 | http.HandleFunc("/hostInfo", hostInfoApiHandler) 80 | http.HandleFunc("/systemd", systemdHandler) 81 | log.Fatal(http.ListenAndServe(":"+strconv.FormatInt(configJson.Port, 10), nil)) 82 | } 83 | 84 | // This function will read config file and load the json into memory 85 | func loadConfigJson() { 86 | file, err := ioutil.ReadFile(configJsonPath) 87 | if err != nil { 88 | fmt.Printf("Config file not found.\n") 89 | os.Exit(1) 90 | } 91 | _ = json.Unmarshal([]byte(file), &configJson) 92 | } 93 | 94 | // This function will create a map (using the data in config json) to 95 | // store aliases and their corresponding paths and store it in memory. 96 | func createAliasMap() { 97 | aliasesTemp := make(map[string]string) 98 | for _, alias := range configJson.Aliases { 99 | aliasesTemp[alias.AliasName] = alias.AliasTo 100 | } 101 | aliases = aliasesTemp 102 | } 103 | 104 | // Function to show API Doc 105 | func apiDocHandler(w http.ResponseWriter, r *http.Request) { 106 | response := apiDoc.ApiDocString 107 | fmt.Fprintf(w, response) 108 | } 109 | 110 | // This is the handler for serving aliases. It returns the 111 | // alias map as a list 112 | func aliasHandler(w http.ResponseWriter, r *http.Request) { 113 | response := allAliases() 114 | fmt.Fprintf(w, response) 115 | } 116 | 117 | // This function creates a string representing the alias map with 118 | // proper formatting. 119 | func allAliases() string { 120 | aliasesSlice := make([]string, 0, 10) 121 | for key, value := range aliases { 122 | aliasesSlice = append(aliasesSlice, key+" : "+value) 123 | } 124 | 125 | if len(aliasesSlice) != 0 { 126 | aliasesString := strings.Join(aliasesSlice, "\n") 127 | return aliasesString 128 | } 129 | 130 | return "" 131 | } 132 | 133 | func systemdListHandler(w http.ResponseWriter, r *http.Request) { 134 | 135 | filterBySlice, isFilterBySlice := r.URL.Query()["filterBy"] 136 | filterBy := []string{} 137 | if !isFilterBySlice { 138 | filterBy = []string{"*"} 139 | } else { 140 | filterBy = strings.Split(filterBySlice[0], ",") 141 | } 142 | 143 | listSystemdServices, _ := systemdDriver.ListSystemdServices(filterBy) 144 | 145 | listSystemdServicesResponse := models.ListSystemdResponseHolder{} 146 | listSystemdServicesResponse.Services = *listSystemdServices 147 | listSystemdServicesResponse.NumServices = len(*listSystemdServices) 148 | listSystemdServicesResponse.Timestamp = time.Now().UTC().Unix() 149 | listSystemdServicesResponse.TimestampUTC = time.Now().UTC().String() 150 | 151 | listSystemdServicesJson, _ := json.Marshal(listSystemdServicesResponse) 152 | 153 | w.Write(listSystemdServicesJson) 154 | } 155 | 156 | 157 | 158 | func systemdServicesHandler(w http.ResponseWriter, r *http.Request) { 159 | 160 | serviceNameSlice, isServiceNameSlice := r.URL.Query()["serviceName"] 161 | operationSlice, isOperationSlice := r.URL.Query()["operation"] 162 | 163 | operation, serviceName := "", "" 164 | 165 | if isServiceNameSlice { 166 | serviceName = serviceNameSlice[0] 167 | } 168 | 169 | if isOperationSlice { 170 | operation = operationSlice[0] 171 | } 172 | 173 | operateSystemdServiceResponse := models.OperateSytemdResponseHolder{} 174 | 175 | status := "" 176 | 177 | if operation == "start" { 178 | status, _ = systemdDriver.StartSystemdService(serviceName) 179 | } else if operation == "stop" { 180 | status, _ = systemdDriver.StopSystemdService(serviceName) 181 | } 182 | 183 | operateSystemdServiceResponse.Status = status 184 | operateSystemdServiceResponse.Timestamp = time.Now().UTC().Unix() 185 | operateSystemdServiceResponse.TimestampUTC = time.Now().UTC().String() 186 | 187 | operateSystemdJson, _ := json.Marshal(operateSystemdServiceResponse) 188 | 189 | w.Write(operateSystemdJson) 190 | } 191 | 192 | func systemdHandler(w http.ResponseWriter, r *http.Request) { 193 | 194 | w.Header().Set("Content-Type", "application/json") 195 | 196 | operationSlice, isOperationSlice := r.URL.Query()["operation"] 197 | operation := "" 198 | 199 | if isOperationSlice { 200 | operation = operationSlice[0] 201 | } 202 | 203 | if operation == "list" { 204 | systemdListHandler(w, r) 205 | } else if operation == "start" || operation == "stop" { 206 | systemdServicesHandler(w, r) 207 | } 208 | } 209 | 210 | func hostInfoApiHandler(w http.ResponseWriter, r *http.Request) { 211 | 212 | hostInfo, _ := hostInfoDriver.HostInfo() 213 | w.Header().Set("Content-Type", "application/json") 214 | 215 | hostInfoResponse := models.HostInfo{} 216 | hostInfoResponse.HostInfo = *hostInfo 217 | hostInfoResponse.Timestamp = time.Now().UTC().Unix() 218 | hostInfoResponse.TimestampUTC = time.Now().UTC().String() 219 | 220 | hostInfoJson, _ := json.Marshal(hostInfoResponse) 221 | 222 | w.Write(hostInfoJson) 223 | } 224 | 225 | func sysMetricApihandler(w http.ResponseWriter, r *http.Request) { 226 | 227 | sysMetrics := sysMetricDriver.GetSystemMetrics() 228 | w.Header().Set("Content-Type", "application/json") 229 | 230 | systemMetricResponse := models.SystemMetricsResponse{} 231 | systemMetricResponse.Metrics = *sysMetrics 232 | systemMetricResponse.Timestamp = time.Now().UTC().Unix() 233 | systemMetricResponse.TimestampUTC = time.Now().UTC().String() 234 | 235 | metricsJson, _ := json.Marshal(systemMetricResponse) 236 | 237 | w.Write(metricsJson) 238 | } 239 | 240 | func procApiHandler(w http.ResponseWriter, r *http.Request) { 241 | 242 | procPidSlice, isProcPidSlice := r.URL.Query()["pid"] 243 | regexSlice, isRegexSlice := r.URL.Query()["regex"] 244 | filterBySlice, isFilterBySlice := r.URL.Query()["filterBy"] 245 | 246 | w.Header().Set("Content-Type", "application/json") 247 | 248 | if !isProcPidSlice { 249 | procListResponse := models.ProcListResponse{} 250 | 251 | filterBy, regex := "", "" 252 | if isRegexSlice { 253 | regex = regexSlice[0] 254 | } 255 | 256 | if isFilterBySlice { 257 | filterBy = filterBySlice[0] 258 | } 259 | 260 | procs, _ := procDriver.GetListOfProcesses(filterBy, regex) 261 | procListResponse.ProcList = *procs 262 | procListResponse.Timestamp = time.Now().UTC().Unix() 263 | procListResponse.TimestampUTC = time.Now().UTC().String() 264 | 265 | procsJson, _ := json.Marshal(procListResponse) 266 | 267 | w.Write(procsJson) 268 | 269 | return 270 | } 271 | 272 | pidstr := procPidSlice[0] 273 | pid, _ := strconv.Atoi(pidstr) 274 | proc, _ := procDriver.GetProcessDetails(int32(pid)) 275 | 276 | procDescriptionResponse := models.ProcDescriptionResponse{} 277 | procDescriptionResponse.ProcDesc = *proc 278 | procDescriptionResponse.Timestamp = time.Now().UTC().Unix() 279 | procDescriptionResponse.TimestampUTC = time.Now().UTC().String() 280 | 281 | procJson, _ := json.Marshal(procDescriptionResponse) 282 | 283 | w.Write(procJson) 284 | } 285 | 286 | // This is the handler for root. It takes in a number of URL query params 287 | // and it returns resource accordingly. 288 | // It takes the followung URL params : 289 | // path : The path (absolute) to the resource to be viewed. For example it can be path to 290 | // a log file 291 | // readFrom : It specifies the end from which the resource is to be read - 292 | // head or tail. Accordingly it can take only two values: head|tail 293 | // limit : It denotes the number of lines to be read from that resource. 294 | // For example the number of lines of a log file to be read. 295 | // It must be a integer greater than 0 296 | // filterBy : The value should be a regex. The regex will be used to filter lines 297 | // from the resource specified and only those lines will be returned. 298 | // ignore : The value should be a regex. The regex will be used as a negative 299 | // filter to remove lines which will contain texts matching the regex. 300 | // alias : This represents alias to a path. Must be configured in config json. 301 | func apiHandler(w http.ResponseWriter, r *http.Request) { 302 | 303 | // The URL parameters are extracted and stored in respective variables 304 | pathSlice, isPath := r.URL.Query()["path"] 305 | readFromSlice, isReadFrom := r.URL.Query()["readFrom"] 306 | limitSlice, isLimit := r.URL.Query()["limit"] 307 | posRegexSlice, isPosRegex := r.URL.Query()["filterBy"] 308 | negRegexSlice, isNegRegex := r.URL.Query()["ignore"] 309 | aliasSlice, isAlias := r.URL.Query()["alias"] 310 | 311 | // variable to store request log 312 | requestLog := make([]string, 0, 1) 313 | 314 | // Get remote client 315 | remote_client := r.RemoteAddr 316 | 317 | path, readFrom, limit, posRegex, negRegex, alias := "", "tail", int64(10), "", "", "" 318 | 319 | if isPath { 320 | path = pathSlice[0] 321 | requestLog = append(requestLog, "path : " + path) 322 | } 323 | 324 | if isReadFrom { 325 | readFrom = readFromSlice[0] 326 | requestLog = append(requestLog, "readFrom : " + readFrom) 327 | } 328 | 329 | // Convert the limit to int64 type and return error if any during conversion 330 | if isLimit { 331 | limitTemp, err := strconv.ParseInt(limitSlice[0], 10, 64) 332 | if err != nil { 333 | logger.WithFields(logrus.Fields{ 334 | "remote_client": remote_client, 335 | }).Error(err.Error()) 336 | fmt.Fprintf(w, err.Error()) 337 | return 338 | } 339 | limit = limitTemp 340 | requestLog = append(requestLog, "limit : " + limitSlice[0]) 341 | } 342 | 343 | if isPosRegex { 344 | posRegex = posRegexSlice[0] 345 | requestLog = append(requestLog, "filterBy : " + posRegex) 346 | } 347 | 348 | if isNegRegex { 349 | negRegex = negRegexSlice[0] 350 | requestLog = append(requestLog, "ignore : " + negRegex) 351 | } 352 | 353 | if isAlias { 354 | alias = aliasSlice[0] 355 | requestLog = append(requestLog, "alias : " + alias) 356 | } 357 | 358 | // Store all the URL params in QueryHolder struct. 359 | // This is for easy handling of the request 360 | request := &models.QueryHolder{ 361 | Path: path, 362 | Alias: alias, 363 | ReadFrom: readFrom, 364 | Limit: limit, 365 | Regex: posRegex, 366 | NegateRegex: negRegex, 367 | Grep: "", 368 | } 369 | 370 | // Log request 371 | logger.WithFields(logrus.Fields{ 372 | "remote_client": remote_client, 373 | }).Info(strings.Join(requestLog, " ")) 374 | 375 | // Get the response for the current request and write it to the response 376 | // of the current request and send it ot user. If FIleDriver returns 377 | // any error then send it to user. 378 | response, err := fileDriver.FileDriver(request, aliases, &configJson, logger) 379 | if err != nil { 380 | logger.WithFields(logrus.Fields{ 381 | "remote_client": remote_client, 382 | }).Error(err.Error()) 383 | fmt.Fprintf(w, err.Error()) 384 | return 385 | } 386 | 387 | // Send response back to user 388 | fmt.Fprintf(w, response) 389 | } 390 | -------------------------------------------------------------------------------- /conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "allow_all": true, 4 | "block_for": [ 5 | "" 6 | ], 7 | "allow_for": [ 8 | "" 9 | ], 10 | "aliases": [] 11 | } 12 | -------------------------------------------------------------------------------- /conf/vision.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Systemd service for vision 3 | After=network.target 4 | 5 | [Service] 6 | User=root 7 | Group=root 8 | ExecStart=/usr/bin/vision 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /core/apiDoc/apiDoc.go: -------------------------------------------------------------------------------- 1 | package apiDoc 2 | 3 | var ApiDocString string 4 | 5 | func prepareDoc() { 6 | 7 | // The doc body 8 | apiDocString := ` 9 | GET options: 10 | path : String Absolute path of the resource file on the remote system 11 | readFrom : String Specifies from where to read the file, can be wither of head ot tail 12 | limit : Integer Specifies the number of lines to be read 13 | filterBy : String (regex) A regex to filter out desired lines from the given file. Only thoe lines containing patterns matched by the given regex will be returned. 14 | ignore : String (regex) A regex to exclude lines containing patterns matching the regex 15 | alias : String An alias name. Must be configured beforehand 16 | ` 17 | 18 | ApiDocString = apiDocString 19 | } 20 | 21 | func init() { 22 | prepareDoc() 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /core/fileDriver/fileDriver.go: -------------------------------------------------------------------------------- 1 | // This package generates response for a iven request 2 | package fileDriver 3 | 4 | import ( 5 | "vision/core/fileHandler" 6 | "vision/core/models" 7 | "vision/core/util" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // This function processes the requests and generates the response. It uses sanitise function 12 | // to sanitise the requests and then uses fileHandler package to generate response. 13 | // Params: 14 | // request : Struct of type QuireyHolder containing all the URL params 15 | // aliases : Alias map 16 | // configJson : Struct of type ConfigModel containing all the config params 17 | func FileDriver(request *models.QueryHolder, aliases map[string]string, configJson *models.ConfigModel, logger *logrus.Logger) (string, error) { 18 | 19 | // Sanitise the request 20 | isClean, err := request.Sanitise(aliases) 21 | if err != nil || !isClean { 22 | return "", err 23 | } 24 | 25 | filePath := "" 26 | 27 | // Set resource path, path param is given more preference than alias param. 28 | // So if both path and alias are there in URL params, path is chosen. 29 | if request.Path != "" { 30 | filePath = request.Path 31 | } else if request.Alias != "" { 32 | filePath = aliases[request.Alias] 33 | } 34 | 35 | // Evaluate Acls and check it the current resource is allowed to be viewed. 36 | // If allowed then proceed, if not then send suitable message back to user 37 | errAcl := util.CheckAcls(filePath, configJson) 38 | if errAcl != nil { 39 | logger.Error("Access right violation for path : ", filePath) 40 | return "", errAcl 41 | } 42 | 43 | // Read from head or from tail as the request may be. 44 | if request.ReadFrom == "head" { 45 | return fileHandler.ReadFromHead(filePath, request.Regex, request.NegateRegex, request.Limit, logger) 46 | } else { 47 | return fileHandler.ReadFromTail(filePath, request.Regex, request.NegateRegex, request.Limit, logger) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/fileHandler/head.go: -------------------------------------------------------------------------------- 1 | // Package containing method to process resources 2 | package fileHandler 3 | 4 | import ( 5 | "os" 6 | "bufio" 7 | "io" 8 | "strings" 9 | "vision/core/util" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // This function will read a given resource starting from head uptil a given numer 14 | // of lines as specified in limit 15 | // Params: 16 | // path : Path to the resource on filesystem 17 | // posRegex : The regex to filter lines 18 | // negregex : The regex to exclude lines 19 | // numLines : the number of lines to limit to 20 | func ReadFromHead(path, posRegex, negRegex string, numLines int64, logger *logrus.Logger) (string, error) { 21 | fileHandle, err := os.Open(path) 22 | if err != nil { 23 | return "", err 24 | } 25 | defer fileHandle.Close() 26 | 27 | linesList := make([]string, 0, 1) 28 | 29 | bufferedReader := bufio.NewReader(fileHandle) 30 | var line string 31 | 32 | for index := int64(0); index < numLines; index++ { 33 | line, err = bufferedReader.ReadString('\n') 34 | if err == io.EOF { 35 | break 36 | } 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | // Use CheckPattern function to apply the provided regexes 42 | if util.CheckPattern(line, posRegex, negRegex) { 43 | linesList = append(linesList, line) 44 | } 45 | } 46 | 47 | topNlines := strings.Join(linesList[:], "") 48 | 49 | return topNlines, nil 50 | } 51 | -------------------------------------------------------------------------------- /core/fileHandler/head_test.go: -------------------------------------------------------------------------------- 1 | package fileHandler 2 | 3 | import ( 4 | "testing" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func createTestFile() { 10 | 11 | // Content for test file 12 | fileContent := `This is a test file for performing unit testing 13 | on the functions provided in this package. Before 14 | each test function runs, this content will be dumped 15 | to a file named test-file.txt. Once the test case has 16 | been executed, this file will be removed. 17 | Vision is a light weight tool written purely in 18 | golang for viewing remote resources over HTTP. 19 | Vision allows you to view 20 | config files, log files and other such 21 | files over HTTP via your browser or on your terminal. 22 | It allows you to set ACLs via 23 | which you can block view on certain resources and 24 | alow view on certain resources. It allows you toconfigure aliases 25 | so that you do not have to type the entire path of the resource 26 | on server, view a file from top, or bottom, apply regex 27 | for filtering contents and specify number of lines to be 28 | read form desired files. 29 | ` 30 | 31 | f, _ := os.Create("test-file.txt") 32 | f.WriteString(fileContent) 33 | f.Close() 34 | } 35 | 36 | func removeTestFile() { 37 | 38 | //Remove test-file.txt 39 | os.Remove("test-file.txt") 40 | } 41 | 42 | func TestHead(t *testing.T) { 43 | 44 | // setup test file 45 | createTestFile() 46 | 47 | content, err := ReadFromHead("./test-file.txt", "", "", 5, nil) 48 | if err != nil { 49 | t.Errorf(err.Error()) 50 | } 51 | 52 | lines := strings.Split(content, "\n") 53 | numLines := len(lines) - 1 54 | 55 | if numLines != 5 { 56 | t.Errorf("Number of lines = %d, Expected 5", numLines) 57 | } 58 | 59 | content, err = ReadFromHead("./test-file.txt", "", "", 100, nil) 60 | if err != nil { 61 | t.Errorf(err.Error()) 62 | } 63 | 64 | lines = strings.Split(content, "\n") 65 | numLines = len(lines) - 1 66 | 67 | if numLines != 17 { 68 | t.Errorf("Number of lines = %d, Expected 17", numLines) 69 | } 70 | 71 | // remove test file 72 | removeTestFile() 73 | } 74 | 75 | func TestHeadWithPosFiler(t *testing.T) { 76 | 77 | // setting up test file 78 | createTestFile() 79 | 80 | content, err := ReadFromHead("./test-file.txt", "HTTP", "", 100, nil) 81 | if err != nil { 82 | t.Errorf(err.Error()) 83 | } 84 | 85 | lines := strings.Split(content, "\n") 86 | numLines := len(lines) - 1 87 | 88 | if numLines != 2 { 89 | t.Errorf("Number of lines = %d, Expected 1", numLines) 90 | } 91 | 92 | if !strings.Contains(lines[0], "HTTP") { 93 | t.Errorf("Expected HTTP, however not found") 94 | } 95 | 96 | // removing test file 97 | removeTestFile() 98 | } 99 | 100 | func TestHeadWithNegFiler(t *testing.T) { 101 | 102 | // setting up test file 103 | createTestFile() 104 | 105 | content, err := ReadFromHead("./test-file.txt", "", "HTTP", 100, nil) 106 | if err != nil { 107 | t.Errorf(err.Error()) 108 | } 109 | 110 | lines := strings.Split(content, "\n") 111 | numLines := len(lines) - 1 112 | 113 | if numLines != 15 { 114 | t.Errorf("Number of lines = %d, Expected 15", numLines) 115 | } 116 | 117 | linesConcatinated := strings.Join(lines, "") 118 | if strings.Contains(linesConcatinated, "HTTP") { 119 | t.Errorf("HTTP not expected, however got it") 120 | } 121 | 122 | // removing test file 123 | removeTestFile() 124 | } 125 | 126 | func TestHeadWithRegexFiler(t *testing.T) { 127 | 128 | // setting up test file 129 | createTestFile() 130 | 131 | content, err := ReadFromHead("./test-file.txt", "HTTP|ACLs", "", 100, nil) 132 | if err != nil { 133 | t.Errorf(err.Error()) 134 | } 135 | 136 | lines := strings.Split(content, "\n") 137 | numLines := len(lines) - 1 138 | 139 | if numLines != 3 { 140 | t.Errorf("Number of lines = %d, Expected 3", numLines) 141 | } 142 | 143 | linesConcatinated := strings.Join(lines, "") 144 | t.Logf(linesConcatinated) 145 | if !(strings.Contains(linesConcatinated, "HTTP") && strings.Contains(linesConcatinated, "ACLs")) { 146 | t.Errorf("HTTP and ACLs expected, however not found") 147 | } 148 | 149 | // removing test file 150 | removeTestFile() 151 | } 152 | -------------------------------------------------------------------------------- /core/fileHandler/tail.go: -------------------------------------------------------------------------------- 1 | // Package containing method to process resources 2 | package fileHandler 3 | 4 | import ( 5 | "os" 6 | "io" 7 | "strings" 8 | "vision/core/util" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // This function takes a string of lines. It splits the string into a list 13 | // of lines. Then it filetrs the list to get the desired lines. Once it 14 | // has the desired list of lines it joins them into a single string and 15 | // returns it back. 16 | // Params: 17 | // lines: String of extracted lines 18 | // posRegex : regex for filtering 19 | // negRegex : regex for excluding 20 | func getfilteredLines(lines, posRegex, negRegex string, logger *logrus.Logger) (string) { 21 | lineList := strings.Split(lines, "\n") 22 | filteredLines := make([]string, 0, 1) 23 | 24 | for _, line := range lineList { 25 | if util.CheckPattern(line, posRegex, negRegex) { 26 | filteredLines = append(filteredLines, line) 27 | } 28 | } 29 | allLines := strings.Join(filteredLines, "\n") 30 | if len(filteredLines) == 0 { 31 | return "" 32 | } 33 | if allLines[len(allLines) - 1] == 0 { 34 | allLines = string(allLines[0: len(allLines) - 1]) 35 | } 36 | return allLines 37 | } 38 | 39 | // This function will read a given resource starting from tail uptil a given numer 40 | // of lines as specified in limit 41 | // Params: 42 | // path : Path to the resource on filesystem 43 | // posRegex : The regex to filter lines 44 | // negregex : The regex to exclude lines 45 | // numLines : the number of lines to limit to 46 | func ReadFromTail(path, posRegex, negRegex string, numLines int64, logger *logrus.Logger) (string, error) { 47 | fileHandle, err := os.Open(path) 48 | if err != nil { 49 | return "", err 50 | } 51 | defer fileHandle.Close() 52 | 53 | numNewLines := int64(0) 54 | var offset int64 = -1 55 | var finalReadStartPos int64 56 | for numNewLines <= numLines-1 { 57 | startPos, err := fileHandle.Seek(offset, 2) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | if startPos == 0 { 63 | finalReadStartPos = -1 64 | break 65 | } 66 | 67 | b := make([]byte, 1) 68 | _, err = fileHandle.ReadAt(b, startPos) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | if offset == int64(-1) && string(b) == "\n" { 74 | offset-- 75 | continue 76 | } 77 | 78 | if string(b) == "\n" { 79 | numNewLines++ 80 | finalReadStartPos = startPos 81 | } 82 | 83 | offset-- 84 | } 85 | 86 | endPos, err := fileHandle.Seek(int64(-1), 2) 87 | if err != nil { 88 | return "", err 89 | } 90 | b := make([]byte, (endPos+1)-finalReadStartPos) 91 | _, err = fileHandle.ReadAt(b, finalReadStartPos+1) 92 | if err == io.EOF { 93 | return getfilteredLines(string(b), posRegex, negRegex, logger), nil 94 | } else if err != nil { 95 | return "", err 96 | } 97 | 98 | return "**No error but no text read.**", nil 99 | } 100 | -------------------------------------------------------------------------------- /core/fileHandler/tail_test.go: -------------------------------------------------------------------------------- 1 | package fileHandler 2 | 3 | import ( 4 | "testing" 5 | "strings" 6 | ) 7 | 8 | func TestTail(t *testing.T) { 9 | 10 | // setup test file 11 | createTestFile() 12 | 13 | content, err := ReadFromTail("./test-file.txt", "", "", 5, nil) 14 | if err != nil { 15 | t.Errorf(err.Error()) 16 | } 17 | 18 | lines := strings.Split(content, "\n") 19 | numLines := len(lines) - 1 20 | 21 | if numLines != 5 { 22 | t.Errorf("Number of lines = %d, Expected 5", numLines) 23 | } 24 | 25 | if !(lines[4] == "read form desired files.") { 26 | t.Errorf("Expected 'read form desired files' at last line, however not found") 27 | } 28 | 29 | content, err = ReadFromHead("./test-file.txt", "", "", 100, nil) 30 | if err != nil { 31 | t.Errorf(err.Error()) 32 | } 33 | 34 | lines = strings.Split(content, "\n") 35 | numLines = len(lines) - 1 36 | 37 | if numLines != 17 { 38 | t.Errorf("Number of lines = %d, Expected 17", numLines) 39 | } 40 | 41 | // remove test file 42 | removeTestFile() 43 | } 44 | -------------------------------------------------------------------------------- /core/hostInfoDriver/hostInfoDriver.go: -------------------------------------------------------------------------------- 1 | package hostInfoDriver 2 | 3 | import ( 4 | "vision/core/hostInfoHandler" 5 | "github.com/shirou/gopsutil/host" 6 | ) 7 | 8 | func HostInfo() (*host.InfoStat, error) { 9 | return hostInfoHandler.GetHostInfo() 10 | } 11 | -------------------------------------------------------------------------------- /core/hostInfoHandler/hostInfoHandler.go: -------------------------------------------------------------------------------- 1 | package hostInfoHandler 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/host" 5 | ) 6 | 7 | func GetHostInfo() (*host.InfoStat, error) { 8 | return host.Info() 9 | } 10 | -------------------------------------------------------------------------------- /core/models/configModel.go: -------------------------------------------------------------------------------- 1 | // Package containing struct definitions for holding configs and params used 2 | // in vision 3 | package models 4 | 5 | // Struct for holding aliases - alias name and actual path 6 | type alias struct { 7 | AliasName string `json:"alias_name"` 8 | AliasTo string `json:"alias_to"` 9 | } 10 | 11 | // Struct for holding the config json declared in config file 12 | type ConfigModel struct { 13 | Port int64 `json:"port"` 14 | AllowAll bool `json:"allow_all"` 15 | BlockFor []string `json:"block_for"` 16 | AllowFor []string `json:"allow_for"` 17 | Aliases []alias `json:"aliases"` 18 | } 19 | -------------------------------------------------------------------------------- /core/models/hostInfoModel.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/host" 5 | ) 6 | 7 | type HostInfo struct { 8 | HostInfo host.InfoStat 9 | Timestamp int64 10 | TimestampUTC string 11 | } 12 | -------------------------------------------------------------------------------- /core/models/model.go: -------------------------------------------------------------------------------- 1 | // Package containing struct definitions for holding configs and params used 2 | // in vision 3 | package models 4 | 5 | import ( 6 | "os" 7 | "errors" 8 | ) 9 | 10 | type QueryHolder struct { 11 | // path to the file 12 | Path string 13 | 14 | // alias name for a file. must be configured 15 | Alias string 16 | 17 | // can accept only two values : head or tail 18 | ReadFrom string 19 | 20 | // number of lines to be streamed 21 | Limit int64 22 | 23 | // search for lines containing entities matching given regex 24 | Regex string 25 | 26 | // search for lines containing entities which does not match given regex 27 | NegateRegex string 28 | 29 | // grep command options 30 | Grep string 31 | } 32 | 33 | // Method to check if the given path is valid. The resource pointed by the 34 | // path should exists and the resource should not be a directory 35 | // Params : 36 | // path : atring containing resource 37 | func isValidPath(path string) (error) { 38 | stat, err := os.Stat(path) 39 | if os.IsNotExist(err) { 40 | return errors.New("FILE_NOT_FOUND") 41 | } 42 | 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if stat.IsDir() { 48 | return errors.New("PATH_IS_A_DIRECTORY") 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // Function to sanitise the URL query params 55 | // Params: 56 | // aliases : alias map 57 | func (queryHolder *QueryHolder) Sanitise(aliases map[string]string) (bool, error) { 58 | path := "" 59 | exists := false 60 | 61 | // Both path and alias cannot be defined together 62 | // Also path will be given higher priority over alias 63 | if queryHolder.Path != "" { 64 | path = queryHolder.Path 65 | } else if queryHolder.Alias != "" { 66 | path, exists = aliases[queryHolder.Alias] 67 | if !exists { 68 | return false, errors.New("ALIAS_DOES_NOT_EXISTS") 69 | } 70 | } else { 71 | return false, errors.New("BOTH_PATH_AND_ALIAS_IS_EMPTY") 72 | } 73 | 74 | // checks if path is valid. 75 | err := isValidPath(path) 76 | if err != nil { 77 | return false, err 78 | } 79 | 80 | // check if readFrom is invalid 81 | if !(queryHolder.ReadFrom == "head" || queryHolder.ReadFrom == "tail") { 82 | return false, errors.New("INVALID_READ_POS") 83 | } 84 | 85 | // limit cannot be negative or 0 86 | if queryHolder.Limit <= 0 { 87 | return false, errors.New("INVALID_LIMIT") 88 | } 89 | 90 | return true, nil 91 | } 92 | -------------------------------------------------------------------------------- /core/models/procModels.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type ProcDescriptionShort struct { 8 | Pid int32 9 | Name string 10 | CmdLine string 11 | } 12 | 13 | type ProcDescriptionLong struct { 14 | Pid int32 15 | Ppid int32 16 | Name string 17 | CmdLine string 18 | ExePath string 19 | Cwd string 20 | Status string 21 | Uids []int32 22 | Gids []int32 23 | Nice int32 24 | NumThreads int32 25 | } 26 | 27 | func (p *ProcDescriptionShort) Filter(filterBy, regex string) (bool, error) { 28 | 29 | matchFound := false 30 | 31 | switch filterBy { 32 | case "name": 33 | matchFound, _ = regexp.MatchString(regex, p.Name) 34 | case "cmdline": 35 | matchFound, _ = regexp.MatchString(regex, p.CmdLine) 36 | } 37 | 38 | return matchFound, nil 39 | } 40 | 41 | type ProcListResponse struct { 42 | ProcList []ProcDescriptionShort 43 | Timestamp int64 44 | TimestampUTC string 45 | } 46 | 47 | type ProcDescriptionResponse struct { 48 | ProcDesc ProcDescriptionLong 49 | Timestamp int64 50 | TimestampUTC string 51 | } 52 | -------------------------------------------------------------------------------- /core/models/sysMetricModel.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SystemMetrics struct { 4 | 5 | // type of system metric wanted : can be cpu, memory as of now 6 | CPU CPUMetrics 7 | Memory MemoryMetrics 8 | } 9 | 10 | type CPUMetrics struct { 11 | LoadAvg CPULoadAvgMetrics 12 | } 13 | 14 | type CPULoadAvgMetrics struct { 15 | Load1 float64 16 | Load5 float64 17 | Load15 float64 18 | } 19 | 20 | type MemoryMetrics struct { 21 | VirtualMemory VirtualMemoryMetrics 22 | } 23 | 24 | type VirtualMemoryMetrics struct { 25 | MemTotal uint64 26 | MemFree uint64 27 | UsedPercent float64 28 | } 29 | 30 | type SystemMetricsResponse struct { 31 | Metrics SystemMetrics 32 | Timestamp int64 33 | TimestampUTC string 34 | } 35 | -------------------------------------------------------------------------------- /core/models/systemdModel.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SystemdHolder struct { 4 | ServiceName string 5 | ServiceState string 6 | } 7 | 8 | type ListSystemdResponseHolder struct { 9 | Services []SystemdHolder 10 | NumServices int 11 | Timestamp int64 12 | TimestampUTC string 13 | } 14 | 15 | type OperateSytemdResponseHolder struct { 16 | Status string 17 | Timestamp int64 18 | TimestampUTC string 19 | } 20 | -------------------------------------------------------------------------------- /core/procDriver/procDriver.go: -------------------------------------------------------------------------------- 1 | package procDriver 2 | 3 | import ( 4 | "vision/core/models" 5 | "vision/core/procHandler" 6 | ) 7 | 8 | func GetListOfProcesses(filterBy, regex string) (*[]models.ProcDescriptionShort, error) { 9 | 10 | procList, err := procHandler.ListAllProcs() 11 | 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | procListFilter := make([]models.ProcDescriptionShort, 1) 17 | 18 | for _, proc := range *procList { 19 | if filterBy == "" && regex == "" { 20 | procListFilter = append(procListFilter, proc) 21 | continue 22 | } 23 | 24 | wanted, _ := proc.Filter(filterBy, regex) 25 | 26 | if wanted { 27 | procListFilter = append(procListFilter, proc) 28 | } 29 | } 30 | 31 | return &procListFilter, nil 32 | } 33 | 34 | func GetProcessDetails(pid int32) (*models.ProcDescriptionLong, error) { 35 | 36 | process, err := procHandler.DescribeProc(pid) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return process, nil 43 | } 44 | -------------------------------------------------------------------------------- /core/procHandler/procHandler.go: -------------------------------------------------------------------------------- 1 | package procHandler 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/process" 5 | "vision/core/models" 6 | ) 7 | 8 | func ListAllProcs() (*[]models.ProcDescriptionShort, error) { 9 | 10 | procList := make([]models.ProcDescriptionShort, 1) 11 | processes, _ := process.Processes() 12 | 13 | for _, proc := range processes { 14 | 15 | procName, _ := proc.Name() 16 | procCmdLine, _ := proc.Cmdline() 17 | 18 | procHolder := models.ProcDescriptionShort{ 19 | Pid: proc.Pid, 20 | Name: procName, 21 | CmdLine: procCmdLine, 22 | } 23 | 24 | procList = append(procList, procHolder) 25 | } 26 | 27 | return &procList, nil 28 | } 29 | 30 | func DescribeProc(pid int32) (*models.ProcDescriptionLong, error) { 31 | 32 | proc, err := process.NewProcess(pid) 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | procName, _ := proc.Name() 39 | procCmdLine, _ := proc.Cmdline() 40 | procPpid, _ := proc.Ppid() 41 | procExePath, _ := proc.Exe() 42 | procCwd, _ := proc.Cwd() 43 | procStatus, _ := proc.Status() 44 | procUids, _ := proc.Uids() 45 | procGids, _ := proc.Gids() 46 | procNice, _ := proc.Nice() 47 | procNumThreads, _ := proc.NumThreads() 48 | 49 | procDescription := &models.ProcDescriptionLong{ 50 | Pid: proc.Pid, 51 | Name: procName, 52 | CmdLine: procCmdLine, 53 | Ppid: procPpid, 54 | ExePath: procExePath, 55 | Cwd: procCwd, 56 | Status: procStatus, 57 | Uids: procUids, 58 | Gids: procGids, 59 | Nice: procNice, 60 | NumThreads: procNumThreads, 61 | } 62 | 63 | return procDescription, nil 64 | } 65 | -------------------------------------------------------------------------------- /core/sysMetricDriver/sysMetricDriver.go: -------------------------------------------------------------------------------- 1 | package sysMetricDriver 2 | 3 | import ( 4 | "vision/core/models" 5 | "vision/core/sysMetrichandler" 6 | ) 7 | 8 | func GetSystemMetrics() *models.SystemMetrics { 9 | 10 | return sysMetrichandler.GetSystemMetrics() 11 | } 12 | -------------------------------------------------------------------------------- /core/sysMetrichandler/sysMetrichandler.go: -------------------------------------------------------------------------------- 1 | package sysMetrichandler 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/load" 5 | "github.com/shirou/gopsutil/mem" 6 | "vision/core/models" 7 | ) 8 | 9 | func getCPUMetrics(CPUMetrics *models.CPUMetrics) { 10 | 11 | cpuLoad, error := load.Avg() 12 | 13 | if error == nil { 14 | 15 | CPULoadAvgMetrics := models.CPULoadAvgMetrics{ 16 | Load1: cpuLoad.Load1, 17 | Load5: cpuLoad.Load5, 18 | Load15: cpuLoad.Load15, 19 | } 20 | 21 | CPUMetrics.LoadAvg = CPULoadAvgMetrics 22 | } 23 | } 24 | 25 | func getMemoryMetrics(MemoryMetrics *models.MemoryMetrics) { 26 | 27 | virtualMemory, error := mem.VirtualMemory() 28 | 29 | if error == nil { 30 | 31 | VirtualMemoryMetrics := models.VirtualMemoryMetrics{ 32 | MemTotal: virtualMemory.Total, 33 | MemFree: virtualMemory.Free, 34 | UsedPercent: virtualMemory.UsedPercent, 35 | } 36 | 37 | MemoryMetrics.VirtualMemory = VirtualMemoryMetrics 38 | } 39 | } 40 | 41 | func GetSystemMetrics() *models.SystemMetrics{ 42 | 43 | cpuMetrics := models.CPUMetrics{} 44 | memoryMetrics := models.MemoryMetrics{} 45 | 46 | getCPUMetrics(&cpuMetrics) 47 | getMemoryMetrics(&memoryMetrics) 48 | 49 | SystemMetrics := models.SystemMetrics{ 50 | CPU: cpuMetrics, 51 | Memory: memoryMetrics, 52 | } 53 | 54 | return &SystemMetrics 55 | } 56 | -------------------------------------------------------------------------------- /core/systemdDriver/systemdDriver.go: -------------------------------------------------------------------------------- 1 | package systemdDriver 2 | 3 | import ( 4 | "vision/core/systemdHandler" 5 | "vision/core/models" 6 | ) 7 | 8 | func ListSystemdServices(filterBy []string) (*[]models.SystemdHolder, error) { 9 | 10 | return systemdHandler.ListSystemdServices(filterBy) 11 | } 12 | 13 | func StartSystemdService(target string) (string, error) { 14 | 15 | return systemdHandler.StartSystemdService(target) 16 | } 17 | 18 | func StopSystemdService(target string) (string, error) { 19 | 20 | return systemdHandler.StopSystemdService(target) 21 | } 22 | -------------------------------------------------------------------------------- /core/systemdHandler/systemdHandler.go: -------------------------------------------------------------------------------- 1 | package systemdHandler 2 | 3 | import ( 4 | "vision/core/models" 5 | "github.com/coreos/go-systemd/dbus" 6 | ) 7 | 8 | func ListSystemdServices(filterBy []string) (*[]models.SystemdHolder, error) { 9 | 10 | listSystemdServices := []models.SystemdHolder{} 11 | 12 | sbusConn, err := dbus.New() 13 | 14 | if err != nil { 15 | return &listSystemdServices, err 16 | } 17 | 18 | units, err := sbusConn.ListUnitsByPatterns([]string{}, filterBy) 19 | 20 | if err != nil { 21 | return &listSystemdServices, err 22 | } 23 | 24 | for _, unit := range units { 25 | listSystemdServices = append(listSystemdServices, models.SystemdHolder{ 26 | ServiceName: unit.Name, 27 | ServiceState: unit.ActiveState, 28 | }) 29 | } 30 | 31 | return &listSystemdServices, nil 32 | } 33 | 34 | func isValidService(service string) (bool, error) { 35 | 36 | sbusConn, err := dbus.New() 37 | units, err := sbusConn.ListUnitsByPatterns([]string{}, []string{service}) 38 | 39 | if err != nil { 40 | return false, err 41 | } 42 | 43 | for _, unit := range units { 44 | if unit.Name == service { 45 | return true, nil 46 | } 47 | } 48 | 49 | return false, nil 50 | } 51 | 52 | func StartSystemdService(target string) (string, error) { 53 | 54 | target = target + ".service" 55 | 56 | sbusConn, err := dbus.New() 57 | if err != nil { 58 | return "FAILED", err 59 | } 60 | 61 | servicePresent, err := isValidService(target) 62 | 63 | if err != nil { 64 | return "FAILED", err 65 | } 66 | 67 | if !servicePresent { 68 | return "SERVICE DOES NOT EXIST", err 69 | } 70 | 71 | resChan := make(chan string) 72 | 73 | _, err = sbusConn.StartUnit(target, "replace", resChan) 74 | 75 | if err != nil { 76 | return "FAILED", err 77 | } 78 | 79 | job := <- resChan 80 | 81 | if job != "done" { 82 | return "FAILED", nil 83 | } 84 | 85 | return "OK", nil 86 | } 87 | 88 | func StopSystemdService(target string) (string, error) { 89 | 90 | target = target + ".service" 91 | 92 | sbusConn, err := dbus.New() 93 | if err != nil { 94 | return "FAILED", err 95 | } 96 | 97 | servicePresent, err := isValidService(target) 98 | 99 | if err != nil { 100 | return "FAILED", err 101 | } 102 | 103 | if !servicePresent { 104 | return "SERVICE DOES NOT EXIST", err 105 | } 106 | 107 | resChan := make(chan string) 108 | 109 | _, err = sbusConn.StopUnit(target, "replace", resChan) 110 | 111 | if err != nil { 112 | return "FAILED", err 113 | } 114 | 115 | _ = <- resChan 116 | 117 | return "OK", nil 118 | } 119 | -------------------------------------------------------------------------------- /core/util/checkAcls.go: -------------------------------------------------------------------------------- 1 | // This package contains some utility functions for vision 2 | package util 3 | import ( 4 | "vision/core/models" 5 | "strings" 6 | "errors" 7 | ) 8 | 9 | // Function for checking Acls 10 | // Params: 11 | // path : Path to resource 12 | // configJson : Struct containing config json 13 | func CheckAcls(path string, configJson *models.ConfigModel) (error) { 14 | if configJson.AllowAll == true { 15 | blockFor := configJson.BlockFor 16 | if len(blockFor) == 0 { 17 | return nil 18 | } 19 | for _, blockedEntity := range configJson.BlockFor { 20 | if blockedEntity == "" { 21 | continue 22 | } 23 | if strings.HasPrefix(path, blockedEntity) { 24 | return errors.New("FILE_NOT_ALLOWED_TO_BE_VIEWED") 25 | } 26 | } 27 | 28 | return nil 29 | } else { 30 | allowFor := configJson.AllowFor 31 | if len(allowFor) == 0 { 32 | return errors.New("FILE_NOT_ALLOWED_TO_BE_VIEWED") 33 | } 34 | 35 | for _, allowedEntity := range configJson.AllowFor { 36 | if allowedEntity == "" { 37 | continue 38 | } 39 | if strings.HasPrefix(path, allowedEntity) { 40 | return nil 41 | } 42 | } 43 | 44 | return errors.New("FILE_NOT_ALLOWED_TO_BE_VIEWED") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/util/checkAcls_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "vision/core/models" 6 | ) 7 | 8 | func TestAclsAllowAll(t *testing.T) { 9 | 10 | // Creating test config 11 | testConfig := models.ConfigModel { 12 | AllowAll: true, 13 | BlockFor: []string{"/blocktest/", "/blocktest2/subfolder/"}, 14 | } 15 | 16 | // Create test path 17 | testPath := "/somefolder/test.log" 18 | 19 | err := CheckAcls(testPath, &testConfig) 20 | 21 | if err != nil { 22 | t.Errorf("Expected nil, however got error") 23 | } 24 | 25 | testPath = "/blocktest/test.log" 26 | 27 | err = CheckAcls(testPath, &testConfig) 28 | 29 | if err == nil { 30 | t.Errorf("Expected error, however found nil") 31 | } 32 | 33 | testPath = "/blocktest2/test.log" 34 | 35 | err = CheckAcls(testPath, &testConfig) 36 | 37 | if err != nil { 38 | t.Errorf("Expected nil, however found error") 39 | } 40 | 41 | testPath = "/blocktest2/subfolder/test.log" 42 | 43 | err = CheckAcls(testPath, &testConfig) 44 | 45 | if err == nil { 46 | t.Errorf("Expected Error, however found nil") 47 | } 48 | } 49 | 50 | func TestAclsBlockAll(t *testing.T) { 51 | 52 | // Creating test config 53 | testConfig := models.ConfigModel { 54 | AllowAll: false, 55 | AllowFor: []string{"/allowtest/", "/allowtest2/subfolder"}, 56 | } 57 | 58 | // Create test path 59 | testPath := "/somefolder/test.log" 60 | 61 | err := CheckAcls(testPath, &testConfig) 62 | 63 | if err == nil { 64 | t.Errorf("Expected Error, however found nil") 65 | } 66 | 67 | testPath = "/allowtest/test.log" 68 | 69 | err = CheckAcls(testPath, &testConfig) 70 | 71 | if err != nil { 72 | t.Errorf("Expected nil, however found error") 73 | } 74 | 75 | testPath = "/allowtest2/test.log" 76 | 77 | err = CheckAcls(testPath, &testConfig) 78 | 79 | if err == nil { 80 | t.Errorf("Expected Error, however found nil") 81 | } 82 | 83 | testPath = "/allowtest2/subfolder/test.log" 84 | 85 | err = CheckAcls(testPath, &testConfig) 86 | 87 | if err != nil { 88 | t.Errorf("Expected nil, however found error") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/util/logutil.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // Function to setup logging using using provided file handler 8 | func SetupLogger() *logrus.Logger { 9 | 10 | // Get a new instance of logrus. An instances is being created so 11 | // that it can be passed among functions 12 | logger := logrus.New() 13 | logger.SetFormatter(&logrus.TextFormatter{ 14 | DisableColors: true, 15 | FullTimestamp: true, 16 | }) 17 | 18 | return logger 19 | } 20 | 21 | -------------------------------------------------------------------------------- /core/util/util.go: -------------------------------------------------------------------------------- 1 | // This package contains some utility functions for vision 2 | package util 3 | 4 | import ( 5 | "regexp" 6 | ) 7 | 8 | // Fucntion to check for patters. This fuction is used for filtering lines/texts 9 | // Param : 10 | // line: string containing the line 11 | // posRegex : Regex containing patterns to filter desired texts 12 | // negRegex : Regex containing patterms to exlude desired texts 13 | func CheckPattern(line, posRegex, negRegex string) (bool) { 14 | if posRegex != "" { 15 | match, _ := regexp.MatchString(posRegex, line) 16 | if !match { 17 | return false 18 | } 19 | } 20 | 21 | if negRegex != "" { 22 | match, _ := regexp.MatchString(negRegex, line) 23 | if match { 24 | return false 25 | } 26 | } 27 | 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /core/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUtilPosRegex(t *testing.T) { 8 | 9 | // Create test string 10 | testString := "This is a test string for positive regex match. Testing-1,2,3" 11 | passed := CheckPattern(testString, "regex", "") 12 | 13 | if !passed { 14 | t.Errorf("Expected true, however got false") 15 | } 16 | 17 | passed = CheckPattern(testString, "Testing-[0-9]+.*", "") 18 | 19 | if !passed { 20 | t.Errorf("Expected true, however got false") 21 | } 22 | 23 | passed = CheckPattern(testString, "^Testing-[0-9]+.*$", "") 24 | 25 | if passed { 26 | t.Errorf("Expected false, however got true") 27 | } 28 | 29 | passed = CheckPattern(testString, "absent", "") 30 | } 31 | 32 | func TestUtilNegRegex(t *testing.T) { 33 | 34 | // Creating test string 35 | testString := "This is a test string for negative regex match. Testing-123" 36 | passed := CheckPattern(testString, "", "regex") 37 | 38 | if passed { 39 | t.Errorf("Expected false, however got true") 40 | } 41 | 42 | passed = CheckPattern(testString, "", "Testing-[1-3]+") 43 | 44 | if passed { 45 | t.Errorf("Expected false, however got true") 46 | } 47 | 48 | passed = CheckPattern(testString, "", "absent") 49 | 50 | if !passed { 51 | t.Errorf("Expected true, however got false") 52 | } 53 | } 54 | 55 | func TestUtilPosNegRegex(t *testing.T) { 56 | 57 | // creating test string 58 | testString := "This string will test positive regex and negative regex combined" 59 | passed := CheckPattern(testString, "positive", "negative") 60 | 61 | if passed { 62 | t.Errorf("Expected false, however got true") 63 | } 64 | 65 | passed = CheckPattern(testString, "positive", "absent") 66 | 67 | if !passed { 68 | t.Errorf("Expected true, however got fasle") 69 | } 70 | 71 | passed = CheckPattern(testString, "present", "absent") 72 | 73 | if passed { 74 | t.Errorf("Expected false, however got true") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /main/vision.go: -------------------------------------------------------------------------------- 1 | // This is the main package which contains the entry point to Vision 2 | package main 3 | 4 | // Import the vision API which serves requests 5 | import ( 6 | "vision/api" 7 | ) 8 | 9 | // Main function which starts the API process 10 | func main() { 11 | api.Api() 12 | } 13 | --------------------------------------------------------------------------------